大家好,又见面了,我是你们的朋友全栈君。
如题,要使用JWT实现单点登录功能,只实现了一个简单的注册、登录功能。
目录
思路
以注册功能为例,前端注册平台,向后端发送用户名密码,后端保存到数据库,并且利用JWT生成一串token返回给前端,注册拦截器,此后前端每次访问后端接口,都会经过拦截器,拦截器对token进行解析,成功则继续逻辑,失败则返回错误信息。失效则需要重新登录。登录功能和注册功能差不多,只是一个查询,一个保存,其他逻辑相同。
注册功能
界面展示以及代码逻辑
前端代码很简单,这里就不详细说前端了。主要就是做了两个输入框以及一个提交按钮,如果哪里写的不正确,欢迎前端大神们指正,代码如下:
<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算法加密
注册成功后将token数据返回给前端了,用于用户进入平台进行操作时使用
中间插一段,这里介绍一下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就形成了。
单点登录
示例
实现一个单点登录功能,获取用户信息。
注册拦截器验证Token
后端返回给前端token之后,前端每次访问后端,将token信息放在头信息中,后端创建拦截器,拦截前端传给后端的参数,并且解析,比对token信息是否正确。
前端token存放:
传值
后端拦截器:
拦截配置类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账号...