From df640c4e18d2060995f6f0b6016c4525f952adfe Mon Sep 17 00:00:00 2001 From: mr <1570435771@qq.com> Date: Tue, 8 Jun 2021 00:50:27 +0800 Subject: [PATCH] =?UTF-8?q?add=E7=94=9F=E6=88=90JWT=20Token=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../youlai/auth/config/JwtConfiguration.java | 29 +++++ .../auth/controller/OAuthController.java | 10 +- .../youlai/auth/jwt/JwtPayloadBuilder.java | 102 +++++++++++++++ .../com/youlai/auth/jwt/JwtProperties.java | 43 +++++++ .../youlai/auth/jwt/JwtTokenGenerator.java | 116 ++++++++++++++++++ .../com/youlai/auth/jwt/JwtTokenPair.java | 16 +++ .../com/youlai/auth/jwt/KeyPairFactory.java | 19 +++ .../com/youlai/auth/service/WeAppService.java | 23 ++-- 8 files changed, 343 insertions(+), 15 deletions(-) create mode 100644 youlai-auth/src/main/java/com/youlai/auth/config/JwtConfiguration.java create mode 100644 youlai-auth/src/main/java/com/youlai/auth/jwt/JwtPayloadBuilder.java create mode 100644 youlai-auth/src/main/java/com/youlai/auth/jwt/JwtProperties.java create mode 100644 youlai-auth/src/main/java/com/youlai/auth/jwt/JwtTokenGenerator.java create mode 100644 youlai-auth/src/main/java/com/youlai/auth/jwt/JwtTokenPair.java create mode 100644 youlai-auth/src/main/java/com/youlai/auth/jwt/KeyPairFactory.java diff --git a/youlai-auth/src/main/java/com/youlai/auth/config/JwtConfiguration.java b/youlai-auth/src/main/java/com/youlai/auth/config/JwtConfiguration.java new file mode 100644 index 000000000..f53f58c74 --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/config/JwtConfiguration.java @@ -0,0 +1,29 @@ +package com.youlai.auth.config; + +import com.youlai.auth.jwt.JwtProperties; +import com.youlai.auth.jwt.JwtTokenGenerator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * JwtConfiguration + */ +@EnableConfigurationProperties(JwtProperties.class) +@ConditionalOnProperty(prefix = "jwt.config", name = "enabled") +@Configuration +public class JwtConfiguration { + + /** + * Jwt token generator. + * + * @param jwtProperties the jwt properties + * @return the jwt token generator + */ + @Bean + public JwtTokenGenerator jwtTokenGenerator(JwtProperties jwtProperties) { + return new JwtTokenGenerator(jwtProperties); + } + +} diff --git a/youlai-auth/src/main/java/com/youlai/auth/controller/OAuthController.java b/youlai-auth/src/main/java/com/youlai/auth/controller/OAuthController.java index a5b1b9c23..4e3286569 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/controller/OAuthController.java +++ b/youlai-auth/src/main/java/com/youlai/auth/controller/OAuthController.java @@ -4,6 +4,7 @@ import cn.hutool.json.JSONObject; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.youlai.auth.enums.OAuthClientEnum; +import com.youlai.auth.jwt.JwtTokenPair; import com.youlai.auth.service.WeAppService; import com.youlai.common.constant.AuthConstants; import com.youlai.common.result.Result; @@ -15,7 +16,6 @@ import io.swagger.annotations.ApiOperation; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.*; @@ -53,7 +53,7 @@ public class OAuthController { @ApiIgnore Principal principal, @ApiIgnore @RequestParam Map parameters ) throws HttpRequestMethodNotSupportedException { - OAuth2AccessToken oAuth2AccessToken; + JwtTokenPair jwtTokenPair = new JwtTokenPair(); /** * 获取登录认证的客户端ID @@ -67,15 +67,15 @@ public class OAuthController { switch (client) { case WEAPP: // 微信小程序 - oAuth2AccessToken = weAppService.login(principal, parameters); + jwtTokenPair = weAppService.login(parameters); break; case TEST: // knife4j接口测试文档使用 client_id/client_secret : client/123456 return tokenEndpoint.postAccessToken(principal, parameters).getBody(); default: - oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); +// oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); break; } - return Result.success(oAuth2AccessToken); + return Result.success(jwtTokenPair); } @ApiOperation(value = "注销", notes = "logout") diff --git a/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtPayloadBuilder.java b/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtPayloadBuilder.java new file mode 100644 index 000000000..9586805fa --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtPayloadBuilder.java @@ -0,0 +1,102 @@ +package com.youlai.auth.jwt; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONUtil; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 构建 jwt payload + **/ +public class JwtPayloadBuilder { + + private Map payload = new HashMap<>(); + /** + * 附加的属性 + */ + private Map additional; + /** + * jwt签发者 + **/ + private String iss; + /** + * jwt所面向的用户 + **/ + private String sub; + /** + * 接收jwt的一方 + **/ + private String aud; + /** + * jwt的过期时间,这个过期时间必须要大于签发时间 + **/ + private LocalDateTime exp; + /** + * jwt的签发时间 + **/ + private LocalDateTime iat = LocalDateTime.now(); + /** + * 权限集 + */ + private Set authorities = new HashSet<>(); + /** + * jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击 + **/ + private String jti = IdUtil.simpleUUID(); + + public JwtPayloadBuilder iss(String iss) { + this.iss = iss; + return this; + } + + + public JwtPayloadBuilder sub(String sub) { + this.sub = sub; + return this; + } + + public JwtPayloadBuilder aud(String aud) { + this.aud = aud; + return this; + } + + public JwtPayloadBuilder authorities(Set authorities) { + this.authorities = authorities; + return this; + } + + public JwtPayloadBuilder expDays(int days) { + Assert.isTrue(days > 0, "jwt expireDate must after now"); + this.exp = this.iat.plusDays(days); + return this; + } + + public JwtPayloadBuilder additional(Map additional) { + this.additional = additional; + return this; + } + + public String builder() { + payload.put("iss", this.iss); + payload.put("sub", this.sub); + payload.put("aud", this.aud); + payload.put("exp", this.exp.toEpochSecond(ZoneOffset.of("+8"))); + payload.put("iat", this.iat.toEpochSecond(ZoneOffset.of("+8"))); + payload.put("jti", this.jti); + + if (!CollectionUtils.isEmpty(additional)) { + payload.putAll(additional); + } + payload.put("authorities", this.authorities.toArray()); + return JSONUtil.toJsonStr(JSONUtil.parse(payload)); + + } + +} diff --git a/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtProperties.java b/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtProperties.java new file mode 100644 index 000000000..9ae606209 --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtProperties.java @@ -0,0 +1,43 @@ +package com.youlai.auth.jwt; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * Jwt 在 springboot application.yml 中的配置文件 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "jwt.config") +public class JwtProperties { + /** + * 是否可用 + */ + private boolean enabled; + /** + * jks 路径 + */ + private String keyLocation; + /** + * key alias + */ + private String keyAlias; + /** + * key store pass + */ + private String keyPass; + /** + * jwt签发者 + **/ + private String iss; + /** + * jwt所面向的用户 + **/ + private String sub; + /** + * access jwt token 有效天数 + */ + private int accessExpDays; + +} diff --git a/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtTokenGenerator.java b/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtTokenGenerator.java new file mode 100644 index 000000000..dc8dd790b --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtTokenGenerator.java @@ -0,0 +1,116 @@ +package com.youlai.auth.jwt; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.jwt.Jwt; +import org.springframework.security.jwt.JwtHelper; +import org.springframework.security.jwt.crypto.sign.RsaSigner; +import org.springframework.security.jwt.crypto.sign.RsaVerifier; +import org.springframework.security.jwt.crypto.sign.SignatureVerifier; +import org.springframework.util.Assert; + +import java.security.KeyPair; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Map; +import java.util.Set; + +/** + * JwtTokenGenerator + */ +@Slf4j +public class JwtTokenGenerator { + private static final String JWT_EXP_KEY = "exp"; + private KeyPair keyPair; + private JwtPayloadBuilder jwtPayloadBuilder = new JwtPayloadBuilder(); + private JwtProperties jwtProperties; + + /** + * Instantiates a new Jwt token generator. + * + * @param jwtProperties the jwt properties + */ + public JwtTokenGenerator(JwtProperties jwtProperties) { + this.jwtProperties = jwtProperties; + + KeyPairFactory keyPairFactory = new KeyPairFactory(); + this.keyPair = keyPairFactory.getKeyPair(jwtProperties); + } + + + /** + * Jwt token pair jwt token pair. + * + * @param aud the aud + * @param authorities the authorities + * @param additional the additional + * @return the jwt token pair + */ + public JwtTokenPair jwtTokenPair(String aud, Set authorities, Map additional) { + String accessToken = jwtToken(aud, jwtProperties.getAccessExpDays(), authorities, additional); + + JwtTokenPair jwtTokenPair = new JwtTokenPair(); + jwtTokenPair.setToken_type("bearer"); + jwtTokenPair.setAccess_token(accessToken); + return jwtTokenPair; + } + + /** + * Jwt token string. + * + * @param aud the aud + * @param exp the exp + * @param authorities the authorities + * @param additional the additional + * @return the string + */ + private String jwtToken(String aud, int exp, Set authorities, Map additional) { + String payload = jwtPayloadBuilder + .iss(jwtProperties.getIss()) + .sub(jwtProperties.getSub()) + .aud(aud) + .additional(additional) + .authorities(authorities) + .expDays(exp) + .builder(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + + RsaSigner signer = new RsaSigner(privateKey); + return JwtHelper.encode(payload, signer).getEncoded(); + } + + + /** + * 解码 并校验签名 过期不予解析 + * + * @param jwtToken the jwt token + * @return the jwt claims + */ + public JSONObject decodeAndVerify(String jwtToken) { + Assert.hasText(jwtToken, "jwt token must not be bank"); + RSAPublicKey rsaPublicKey = (RSAPublicKey) this.keyPair.getPublic(); + SignatureVerifier rsaVerifier = new RsaVerifier(rsaPublicKey); + Jwt jwt = JwtHelper.decodeAndVerify(jwtToken, rsaVerifier); + String claims = jwt.getClaims(); + JSONObject jsonObject = JSONUtil.parseObj(claims); + String exp = jsonObject.getStr(JWT_EXP_KEY); + + if (isExpired(exp)) { + throw new IllegalStateException("jwt token is expired"); + } + return jsonObject; + } + + /** + * 判断jwt token是否过期. + * + * @param exp the jwt token exp + * @return the boolean + */ + private boolean isExpired(String exp) { + return LocalDateTime.now().isAfter(LocalDateTime.ofEpochSecond(Long.parseLong(exp), 0, ZoneOffset.ofHours(8))); + } +} diff --git a/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtTokenPair.java b/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtTokenPair.java new file mode 100644 index 000000000..275469a01 --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/jwt/JwtTokenPair.java @@ -0,0 +1,16 @@ +package com.youlai.auth.jwt; + +import lombok.Data; + +import java.io.Serializable; + +/** + * JwtTokenPair + * + **/ +@Data +public class JwtTokenPair implements Serializable { + private static final long serialVersionUID = -8518897818107784049L; + private String access_token; + private String token_type; +} diff --git a/youlai-auth/src/main/java/com/youlai/auth/jwt/KeyPairFactory.java b/youlai-auth/src/main/java/com/youlai/auth/jwt/KeyPairFactory.java new file mode 100644 index 000000000..08620481c --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/jwt/KeyPairFactory.java @@ -0,0 +1,19 @@ +package com.youlai.auth.jwt; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; + +import java.security.KeyPair; + +/** + * KeyPairFactory + **/ +public class KeyPairFactory { + + public KeyPair getKeyPair(JwtProperties jwtProperties) { + KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource(jwtProperties.getKeyLocation()), + jwtProperties.getKeyPass().toCharArray()); + KeyPair keyPair = factory.getKeyPair(jwtProperties.getKeyAlias(), jwtProperties.getKeyPass().toCharArray()); + return keyPair; + } +} diff --git a/youlai-auth/src/main/java/com/youlai/auth/service/WeAppService.java b/youlai-auth/src/main/java/com/youlai/auth/service/WeAppService.java index 8ae28666d..c2fec23c2 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/service/WeAppService.java +++ b/youlai-auth/src/main/java/com/youlai/auth/service/WeAppService.java @@ -4,8 +4,9 @@ import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; import cn.binarywang.wx.miniapp.bean.WxMaUserInfo; import cn.hutool.core.util.StrUtil; +import com.youlai.auth.jwt.JwtTokenGenerator; +import com.youlai.auth.jwt.JwtTokenPair; import com.youlai.auth.enums.PasswordEncoderTypeEnum; -import com.youlai.common.constant.AuthConstants; import com.youlai.common.constant.GlobalConstants; import com.youlai.common.result.Result; import com.youlai.common.result.ResultCode; @@ -17,11 +18,13 @@ import lombok.AllArgsConstructor; import lombok.SneakyThrows; import org.apache.logging.log4j.util.Strings; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.stereotype.Service; +import javax.annotation.Resource; import java.security.Principal; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; /** @@ -37,16 +40,17 @@ public class WeAppService { private PasswordEncoder passwordEncoder; private TokenEndpoint tokenEndpoint; + @Resource + private JwtTokenGenerator jwtTokenGenerator; /** - * @param principal * @param parameters code=小程序授权code * encryptedData=包括敏感数据在内的完整用户信息的加密数据 * iv=加密算法的初始向量 * @return */ @SneakyThrows - public OAuth2AccessToken login(Principal principal, Map parameters) { + public JwtTokenPair login(Map parameters) { String code = parameters.get("code"); @@ -61,6 +65,7 @@ public class WeAppService { String sessionKey = session.getSessionKey(); Result result = memberFeignClient.getUserByOpenid(openid); + Long userId = result.getData().getId(); if (ResultCode.USER_NOT_EXIST.getCode().equals(result.getCode())) { // 微信授权登录 会员信息不存在时 注册会员 String encryptedData = parameters.get("encryptedData"); @@ -86,11 +91,9 @@ public class WeAppService { } } - // oauth2认证参数对应授权登录时注册会员的username、password信息,模拟通过oauth2的密码模式认证生成JWT - parameters.put("username", openid); - parameters.put("password", openid); - - OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); - return oAuth2AccessToken; + HashSet roles = new HashSet<>(); + HashMap additional = new HashMap<>(); + additional.put("userId", String.valueOf(userId)); + return jwtTokenGenerator.jwtTokenPair(openid, roles, additional); } }