JWT单点登录功能

JWT单点登录功能如题,要使用SpringBoot+JWT+VUE+Node.js实现单点登录功能

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

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

目录

思路

注册功能

界面展示以及代码逻辑

MD5的加密算法

JWT生成Token

单点登录

示例

注册拦截器验证Token



思路

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

注册功能

界面展示以及代码逻辑

 

JWT单点登录功能

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

<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>

后端代码:


    @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算法加密

JWT单点登录功能

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

 JWT单点登录功能

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

MD5的加密算法

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

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的简称。它是由是三个部分组成:头部,载体,签名。格式如下:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiQUNDT1VOVCI6IuW8oOS4iSIsImlzcyI6InVzZXIiLCJleHAiOjE2MDY2OTMyODd9.gL2kThdumyBydaal6s7-DCf5GV-1FvioRmrK2R1XhcA

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

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

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

JWT生成token逻辑代码如下:

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

头部以及载体加密

//创建签名,传参,一个是头部,一个是载体  
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加密算法,感觉有些看不懂,欢迎大神指点一下加密算法的逻辑,这里主要介绍签名生成逻辑。

 /**
 * 建立签名
 */
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就形成了。

 

单点登录

示例

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

JWT单点登录功能

 

注册拦截器验证Token

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

前端token存放:

JWT单点登录功能

传值 

JWT单点登录功能

后端拦截器:

JWT单点登录功能

拦截配置类WebConfigurer: 

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:

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


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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

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

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 数据库学习网站

    数据库学习网站Access中国ACCESSXPFAQ网站http://bcd.office-cn.net/对ACCESSXP技术精品文章、FAQ和参考手册都进行整理归类,以便于大家查找和阅读,并

  • java hashmap扩容大小_HashMap 扩容机制

    java hashmap扩容大小_HashMap 扩容机制HashMap:publicHashMap(intinitialCapacity,floatloadFactor){//初始容量不能<0if(initialCapacity<0)thrownewIllegalArgumentException(“Illegalinitialcapacity:”+initialCapacity)…

  • C#QuotedStr方法实现,引号的处理

    C#QuotedStr方法实现,引号的处理在Delphi中处理引号有QuotedStr函数,但C#中却没有相应的处理!所以转换了Delphi的代码为C#中来处理。。以下为代码!protectedstringQuotedStr(stringS){stringvResult;vResult=S;for(inti

    2022年10月17日
  • 怎样选择有效的关键词

    怎样选择有效的关键词

  • 常量池和堆的区别_字符串常量池在堆中还是方法区

    常量池和堆的区别_字符串常量池在堆中还是方法区写在前面:博主是一位普普通通的19届二本大学生,平时最大的爱好就是听听歌,逛逛B站。博主很喜欢的一句话花开堪折直须折,莫待无花空折枝:博主的理解是头一次为人,就应该做自己想做的事,做自己不后悔的事,做自己以后不会留有遗憾的事,做自己觉得有意义的事,不浪费这大好的青春年华。博主写博客目的是记录所学到的知识并方便自己复习,在记录知识的同时获得部分浏览量,得到更多人的认可,满足小小的成就感,同时在写博客的途中结交更多志同道合的朋友,让自己在技术的路上并不孤单。目录:1.常量池与Class常量池2.运.

  • Extjs grid设置单元格字体颜色,及单元格背景色「建议收藏」

    Extjs grid设置单元格字体颜色,及单元格背景色「建议收藏」转自:http://blog.csdn.net/suixufeng/article/details/7480170上面这种是最简单的,设定固定的某单元格中字体颜色。[javascript]viewplaincopy//————————————————–列头    var cm = new

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号