前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JWT单点登录功能

JWT单点登录功能

作者头像
全栈程序员站长
发布2022-08-25 21:42:05
1.2K0
发布2022-08-25 21:42:05
举报
文章被收录于专栏:全栈程序员必看

大家好,又见面了,我是你们的朋友全栈君。

如题,要使用JWT实现单点登录功能,只实现了一个简单的注册、登录功能。

目录

思路

注册功能

界面展示以及代码逻辑

MD5的加密算法

JWT生成Token

单点登录

示例

注册拦截器验证Token

思路

以注册功能为例,前端注册平台,向后端发送用户名密码,后端保存到数据库,并且利用JWT生成一串token返回给前端,注册拦截器,此后前端每次访问后端接口,都会经过拦截器,拦截器对token进行解析,成功则继续逻辑,失败则返回错误信息。失效则需要重新登录。登录功能和注册功能差不多,只是一个查询,一个保存,其他逻辑相同。

注册功能

界面展示以及代码逻辑

前端代码很简单,这里就不详细说前端了。主要就是做了两个输入框以及一个提交按钮,如果哪里写的不正确,欢迎前端大神们指正,代码如下:

代码语言:javascript
复制
<template>
  <div>
    <h1>******用户注册******</h1>
    姓名: <input type="text" name="name" v-model="name"/>
    <br>
    <br>
    密码: <input type="password" name="pwd" v-model="pwd"/>
    <br>
    <label style="color: red;font-size:14px;">{
  
  {message}}</label>
    <br>
    <input type="submit" class="submit" @click="register()">
    <br><br>
    </div>
</template>

<script>
  export default {
    name: 'Register',
    data(){
      return {
        name: '',
        pwd: '',
        message: ''
      }
    },
    methods:{
      register: function() {
        var me = this;
        var url = "/demo/register?name="+ this.name + "&password=" + this.pwd;
        this.$ajax({
          method: 'post',
          url: url,
          contentType: "application/json;charset=utf-8",
          dataType: "text"
        }).then(res =>{
          var data = res.data;
          var token = data.token;
          localStorage.JWT_TOKEN = token;
          this.$router.push({
            path: '/success',
            name: 'Success',
            params: {
              "name": data.user.name,
            }})
        }).catch(function (error) {
          me.message = error.response.data.message;
        });
      }
    }
  }
</script>

<style>
  input{
    border: 1px solid #ccc;
    padding: 7px 0px;
    border-radius: 3px;
    padding-left:5px;
    -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
    box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
    -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
    -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
    transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s
  }
  input:focus{
    border-color: #66afe9;
    outline: 0;
    -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
    box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)
  }
  .submit {
    width: 100px;
    background-color: #66afe9;
  }
</style>

后端代码:

代码语言:javascript
复制
    @PostMapping("/register")
    public HashMap register(@RequestParam("name") String name,
                            @RequestParam("password") String password) throws Exception {
        //保存用户信息
        int res = sysUserService.saveUserInfo(name, password);
        HashMap resultMap = new HashMap();
        //生成token信息
        String token = JWTHS256.buildJWT(name);
        resultMap.put("res", res);
        //前端返回token信息
        resultMap.put("token", token);
        return resultMap;
    }

数据库数据信息,密码经过两次MD5算法加密

注册成功后将token数据返回给前端了,用于用户进入平台进行操作时使用

中间插一段,这里介绍一下MD5加密以及JWT生成token的过程:

MD5的加密算法

这个并不属于单点登录的必要步骤,但是我从网上搜了一下md5的破解,感觉破解起来也是有一定难度的,所以将密码加密保存到数据库还是安全一些。这里是对密码进行两次md5加密保存到数据库

代码语言:javascript
复制
public int saveUserInfo(String name, String pwd) throws Exception {
        SysUser sysUser = new SysUser();
        sysUser.setName(name);
        //MD5加密算法
        sysUser.setPassword(MD5.md5EncodeSecondary(pwd));
        sysUser.setId(UUID.randomUUID().toString());
        return sysUserMapper.save(sysUser);
}
 
public static String md5EncodeSecondary(String noEncrypt) throws Exception {
        return md5Encode(noEncrypt + md5Encode(noEncrypt));
    }

public static String md5Encode(String inStr) throws Exception {
	MessageDigest md5 = null;
	try {
		md5 = MessageDigest.getInstance("MD5");
	} catch (Exception e) {
		System.out.println(e.toString());
		e.printStackTrace();
		return "";
	}

	byte[] byteArray = inStr.getBytes("UTF-8");
	byte[] md5Bytes = md5.digest(byteArray);
	StringBuffer hexValue = new StringBuffer();
	for (int i = 0; i < md5Bytes.length; i++) {
		int val = ((int) md5Bytes[i]) & 0xff;
		if (val < 16) {
			hexValue.append("0");
		}
		hexValue.append(Integer.toHexString(val));
	}
	return hexValue.toString();
}

保存到数据库之后,就是要生成前端访问后端程序的凭证了,也就是JWT加密算法生成的token返回给前端,前端访问后端时将token作为凭证访问后端。

JWT生成Token

JWT,JSON Web Token的简称。它是由是三个部分组成:头部,载体,签名。格式如下:

代码语言:javascript
复制
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiQUNDT1VOVCI6IuW8oOS4iSIsImlzcyI6InVzZXIiLCJleHAiOjE2MDY2OTMyODd9.gL2kThdumyBydaal6s7-DCf5GV-1FvioRmrK2R1XhcA

头部是用于描述关于JWT的基本信息,例如其类型以及用到算法等。JWT有两种加密算法,一是HS256,对称加密算法;另一种是RS256,非对称加密算法。本示例我用的是HS256算法。

载体是存储自定义数据,一般用于存储用户标识,过期时间等信息,是JWT的核心。因为这些数据是后端知道是谁在登录的凭证,而这些数据是存在于token里面的,由token携带,因此后端几乎不需要保存任何数据。

签名是头部base64加密.载体的base64的加密.前两段加入一个密匙用HS256算法或者其他算法加密形成。

JWT生成token逻辑代码如下:

代码语言:javascript
复制
    @PostMapping("/register")
    public HashMap register(@RequestParam("name") String name,
                            @RequestParam("password") String password) throws Exception {
        //保存用户信息
        int res = sysUserService.saveUserInfo(name, password);
        HashMap resultMap = new HashMap();
        //JWT生成token信息
        String token = JWTHS256.buildJWT(name);
        resultMap.put("res", res);
        resultMap.put("token", token);
        return resultMap;
    }

    
    /**
     *   生成token
    **/
    public static String buildJWT(String account) {

        try {
            /**
             * 创建一个32-byte的密钥
             */
            MACSigner macSigner = new MACSigner(SECRET);
            /**
             * 建立payload载体
             */
            JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
                    .subject("user")
                    .issuer("user")
                    .expirationTime(new Date(System.currentTimeMillis()))
                    .claim("ACCOUNT", account)
                    .build();
            /**
             * 建立签名,
                这里创建SignedJWT 对象时,传了两个参数,一个是头部信息,一个是载体
             */
            SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
            signedJWT.sign(macSigner);
            String token = signedJWT.serialize();
            return token;
        } catch (KeyLengthException e) {
            e.printStackTrace();
        } catch (JOSEException e) {
            e.printStackTrace();
        }
        return null;
    }

头部以及载体加密

代码语言:javascript
复制
//创建签名,传参,一个是头部,一个是载体  
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
=====================================================================================

//1.头部生成逻辑
    //初始化一个头部对象JWSHeader
    new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet)
    public JWSHeader(JWSAlgorithm alg) {
        this(alg, (JOSEObjectType)null, (String)null, (Set)null, (URI)null, (JWK)null, (URI)null, (Base64URL)null, (Base64URL)null, (List)null, (String)null, (Map)null, (Base64URL)null);
    }
    public JWSHeader(JWSAlgorithm alg, JOSEObjectType typ, String cty, Set<String> crit, URI jku, JWK jwk, URI x5u, Base64URL x5t, Base64URL x5t256, List<Base64> x5c, String kid, Map<String, Object> customParams, Base64URL parsedBase64URL) {
        super(alg, typ, cty, crit, jku, jwk, x5u, x5t, x5t256, x5c, kid, customParams, parsedBase64URL);
        if (alg.getName().equals(Algorithm.NONE.getName())) {
            throw new IllegalArgumentException("The JWS algorithm \"alg\" cannot be \"none\"");
        }
    }
=====================================================================================

//2.载体生成逻辑:
    //载体生成可以使用的方法
	public static class Builder {
        private final Map<String, Object> claims = new LinkedHashMap();

        public Builder() {
        }

        public Builder(JWTClaimsSet jwtClaimsSet) {
            this.claims.putAll(jwtClaimsSet.claims);
        }
        //JWT签发者
        public JWTClaimsSet.Builder issuer(String iss) {
            this.claims.put("iss", iss);
            return this;
        }
        //JWT面向的用户
        public JWTClaimsSet.Builder subject(String sub) {
            this.claims.put("sub", sub);
            return this;
        }
        //接收JWT的用户,列表
        public JWTClaimsSet.Builder audience(List<String> aud) {
            this.claims.put("aud", aud);
            return this;
        }
        //接收JWT的用户,单个信息
        public JWTClaimsSet.Builder audience(String aud) {
            if (aud == null) {
                this.claims.put("aud", (Object)null);
            } else {
                this.claims.put("aud", Collections.singletonList(aud));
            }

            return this;
        }
        // jwt的过期时间,这个过期时间必须要大于签发时间
        public JWTClaimsSet.Builder expirationTime(Date exp) {
            this.claims.put("exp", exp);
            return this;
        }
        //定义在什么时间之前,该jwt都是不可用的
        public JWTClaimsSet.Builder notBeforeTime(Date nbf) {
            this.claims.put("nbf", nbf);
            return this;
        }
        // jwt的签发时间
        public JWTClaimsSet.Builder issueTime(Date iat) {
            this.claims.put("iat", iat);
            return this;
        }
        //jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
        public JWTClaimsSet.Builder jwtID(String jti) {
            this.claims.put("jti", jti);
            return this;
        }
        //自定义声明
        public JWTClaimsSet.Builder claim(String name, Object value) {
            this.claims.put(name, value);
            return this;
        }

        public JWTClaimsSet build() {
            return new JWTClaimsSet(this.claims, (JWTClaimsSet)null);
        }
    }
    //本次示例中载体使用;
    JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
		    .subject("user")
		    .issuer("user")
		    .expirationTime(new Date(System.currentTimeMillis()))
		    .claim("ACCOUNT", account)
		    .build();	
	可以根据项目需要使用这些注册信息
=====================================================================================

//3.对生成JWSHeader、claimsSet载体对象进行base64加密

    public SignedJWT(JWSHeader header, JWTClaimsSet claimsSet) {
        super(header, new Payload(claimsSet.toJSONObject()));
    }

    public JWSObject(JWSHeader header, Payload payload) {
        if (header == null) {
            throw new IllegalArgumentException("The JWS header must not be null");
        } else {
            this.header = header;
            if (payload == null) {
                throw new IllegalArgumentException("The payload must not be null");
            } else {
                this.setPayload(payload);
                //base64加密,并且设置公共变量-signingInputString,提供后面使用
                this.signingInputString = composeSigningInput(header.toBase64URL(), payload.toBase64URL());
                this.signature = null;
                //设置当前状态为未签名状态,避免重复设置签名
                this.state = JWSObject.State.UNSIGNED;
            }
        }
    }
=====================================================================================
      
//4.组成前两段
private static String composeSigningInput(Base64URL firstPart, Base64URL secondPart) {
   return firstPart.toString() + '.' + secondPart.toString();
}

签名生成

接下来就是对前两段加密生成第三段内容,主要用HMAC加密算法,感觉有些看不懂,欢迎大神指点一下加密算法的逻辑,这里主要介绍签名生成逻辑。

代码语言:javascript
复制
 /**
 * 建立签名
 */
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
signedJWT.sign(macSigner);

//签名生成
public synchronized void sign(JWSSigner signer) throws JOSEException {
	//验证状态是否是未签名状态
	this.ensureUnsignedState();
	//检查当前头部信息是否在缓存中
	this.ensureJWSSignerSupport(signer);

	try {
		//加密,sign方法根据生成密钥的类选择,这里我使用的是MACSigner类,因此使用MACSigner的sign方法
		this.signature = signer.sign(this.getHeader(), this.getSigningInput());
	} catch (JOSEException var3) {
		throw var3;
	} catch (Exception var4) {
		throw new JOSEException(var4.getMessage(), var4);
	}
	//设置签名状态为已签名
	this.state = JWSObject.State.SIGNED;
}

public Base64URL sign(JWSHeader header, byte[] signingInput) throws JOSEException {
	//获取加密算法最小长度
	int minRequiredLength = getMinRequiredSecretLength(header.getAlgorithm());
	if (this.getSecret().length < ByteUtils.byteLength(minRequiredLength)) {
		throw new KeyLengthException("The secret length for " + header.getAlgorithm() + " must be at least " + minRequiredLength + " bits");
	} else {
		//获取加密算法
		String jcaAlg = getJCAAlgorithmName(header.getAlgorithm());
		byte[] hmac = HMAC.compute(jcaAlg, this.getSecret(), signingInput, this.getJCAContext().getProvider());
		//将经过HMAC加密后的数值进行二次加密,获得签名
		return Base64URL.encode(hmac);
	}
}

这样JWT生成的token就形成了。

单点登录

示例

实现一个单点登录功能,获取用户信息。

注册拦截器验证Token

后端返回给前端token之后,前端每次访问后端,将token信息放在头信息中,后端创建拦截器,拦截前端传给后端的参数,并且解析,比对token信息是否正确。

前端token存放:

传值

后端拦截器:

拦截配置类WebConfigurer:

代码语言:javascript
复制
package com.zxy.system.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 配置拦截器
 */
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    /**
     * 拦截静态资源
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    }

    /**
     * 注册拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                //过滤掉不需要拦截的接口
                .excludePathPatterns("/login", "/register","/error");
    }
}

拦截器LoginInterceptor:

代码语言:javascript
复制
package com.zxy.system.config;

import com.zxy.system.util.JWTHS256;
import org.assertj.core.util.Strings;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest,
                             HttpServletResponse httpServletResponse,
                             Object obj) throws IOException {
        String token = httpServletRequest.getHeader("authorization");

        //无token、token失效、token无权限
        if (Strings.isNullOrEmpty(token) || !JWTHS256.validToken(token)) {
            httpServletResponse.setCharacterEncoding("utf-8");
            httpServletResponse.setContentType("text/html; charset=utf-8");
            httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }
        return true;
    }
}


//验证token
public static boolean validToken(String token) {
	try {
		SignedJWT jwt = SignedJWT.parse(token);
		JWSVerifier verifier = new MACVerifier(SECRET);

		//校验是否有效
		if (!jwt.verify(verifier)) {
			//异常抛错
			return false;
		}

		//校验超时
		Date expirationTime = jwt.getJWTClaimsSet().getExpirationTime();
		Date now = new Date();
		if (DateUtil.timeDifference(now, expirationTime) > EXPIRE_TIME) {
			//超时抛错
			return false;
		}

		Object account = jwt.getJWTClaimsSet().getClaim("ACCOUNT");
		//是否有openuid
		if (Objects.isNull(account)) {
			throw ResultException.of(-3, "账号为空");
		}
		return true;
	} catch (ParseException e) {
		e.printStackTrace();
	} catch (JOSEException e) {
		e.printStackTrace();
	}
	return false;
}

单点登录功能就介绍完了。如果有问题或者有更好的建议,欢迎留言评论!

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/143264.html原文链接:https://javaforall.cn

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年5月1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 思路
  • 注册功能
    • 界面展示以及代码逻辑
      • MD5的加密算法
      • JWT生成Token
  • 单点登录
    • 示例
      • 注册拦截器验证Token
相关产品与服务
访问管理
访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档