diff --git a/core/pom.xml b/core/pom.xml index d11a791c042334c801d7d51aff32ca854c2a0fc5..64d01ee9e1dba34d17cbaf17bb2000afd41a9844 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -13,6 +13,7 @@ 1.7.21 + 0.9.0 @@ -20,6 +21,17 @@ slf4j-api ${slf4j.version} + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + + javax.xml.bind + jaxb-api + 2.3.0 + diff --git a/core/src/main/java/com/usthe/sureness/processor/BaseProcessor.java b/core/src/main/java/com/usthe/sureness/processor/BaseProcessor.java index 016381e56919f4fe9b8057045750a71e7bac64ad..9eab66b0e81ed777aa9a98d27c0fb932836ce402 100644 --- a/core/src/main/java/com/usthe/sureness/processor/BaseProcessor.java +++ b/core/src/main/java/com/usthe/sureness/processor/BaseProcessor.java @@ -40,15 +40,17 @@ public abstract class BaseProcessor implements Processor{ * description 认证会调用的接口,在这里面完成认证 * @param var 1 * @return boolean + * @throws SurenessAuthenticationException when发生认证相关异常 */ - public abstract boolean authenticated (SubjectAuToken var); + public abstract boolean authenticated (SubjectAuToken var) throws SurenessAuthenticationException; /** * description 鉴权会调用的接口,在这里面完成鉴权 * @param var 1 * @return boolean + * @throws SurenessAuthorizationException when发生鉴权相关异常 */ - public abstract boolean authorized(SubjectAuToken var); + public abstract boolean authorized(SubjectAuToken var) throws SurenessAuthorizationException; /** * description 当认证鉴权完成通过后,通过token创建subject diff --git a/core/src/main/java/com/usthe/sureness/processor/support/JwtProcessor.java b/core/src/main/java/com/usthe/sureness/processor/support/JwtProcessor.java index b62cb47bbe211d51b67100ef7bbbb5a9ce3a7c02..9386be72e7dbf3c929a27bfc361841bff434cfd4 100644 --- a/core/src/main/java/com/usthe/sureness/processor/support/JwtProcessor.java +++ b/core/src/main/java/com/usthe/sureness/processor/support/JwtProcessor.java @@ -1,9 +1,23 @@ package com.usthe.sureness.processor.support; import com.usthe.sureness.processor.BaseProcessor; +import com.usthe.sureness.processor.exception.IncorrectCredentialsException; +import com.usthe.sureness.processor.exception.SurenessAuthenticationException; +import com.usthe.sureness.processor.exception.SurenessAuthorizationException; import com.usthe.sureness.subject.Subject; import com.usthe.sureness.subject.SubjectAuToken; import com.usthe.sureness.subject.support.JwtSubjectToken; +import com.usthe.sureness.util.JsonWebTokenUtil; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.UnsupportedJwtException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; /** * 支持 appId + jwt 的token的处理器实例 @@ -12,6 +26,8 @@ import com.usthe.sureness.subject.support.JwtSubjectToken; */ public class JwtProcessor extends BaseProcessor { + private static final Logger LOG = LoggerFactory.getLogger(JwtProcessor.class); + @Override public boolean canSupportAuTokenClass(Class var) { return var != null && var == JwtSubjectToken.class; @@ -23,13 +39,45 @@ public class JwtProcessor extends BaseProcessor { } @Override - public boolean authenticated(SubjectAuToken var) { - return false; + public boolean authenticated(SubjectAuToken var) throws SurenessAuthenticationException { + String jwt = (String) var.getCredentials(); + if (jwt == null || "".equals(jwt)) { + throw new IncorrectCredentialsException("this jwt credential is null"); + } + Claims claims = null; + try { + claims = JsonWebTokenUtil.parseJwt(jwt, JsonWebTokenUtil.SECRET_KEY); + } catch (SignatureException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) { + // JWT令牌错误 + if (LOG.isInfoEnabled()) { + LOG.info("jwtProcessor authenticated fail, user: {}, jwt: {}", + var.getPrincipal(), jwt, e); + } + throw new IncorrectCredentialsException("this jwt error:" + e.getMessage()); + } catch (ExpiredJwtException e) { + // JWT 令牌过期 + if (LOG.isDebugEnabled()) { + LOG.debug("jwtProcessor authenticated expired, user: {}, jwt: {}", + var.getPrincipal(), jwt, e); + } + } + if (claims != null) { + String roles = claims.get("roles", String.class); + if (roles != null) { + List roleList = Arrays.asList(roles.split(",")); + var.setSupportRoles(roleList); + } + } + return true; } + @SuppressWarnings("unchecked") @Override - public boolean authorized(SubjectAuToken var) { - return false; + public boolean authorized(SubjectAuToken var) throws SurenessAuthorizationException { + List ownRoles = (List)var.getOwnRoles(); + List supportRoles = (List)var.getSupportRoles(); + return supportRoles == null || supportRoles.isEmpty() + || supportRoles.stream().anyMatch(role -> ownRoles.contains(role)); } @Override diff --git a/core/src/main/java/com/usthe/sureness/processor/support/PasswordProcessor.java b/core/src/main/java/com/usthe/sureness/processor/support/PasswordProcessor.java index 6b85a21f9c4fe243169bf53db9240e9ddbef0f66..99f91ea600441c3d349df8cdfcea912e229c32a7 100644 --- a/core/src/main/java/com/usthe/sureness/processor/support/PasswordProcessor.java +++ b/core/src/main/java/com/usthe/sureness/processor/support/PasswordProcessor.java @@ -1,9 +1,16 @@ package com.usthe.sureness.processor.support; import com.usthe.sureness.processor.BaseProcessor; +import com.usthe.sureness.processor.exception.IncorrectCredentialsException; +import com.usthe.sureness.processor.exception.SurenessAuthenticationException; +import com.usthe.sureness.processor.exception.SurenessAuthorizationException; +import com.usthe.sureness.processor.exception.UnknownAccountException; +import com.usthe.sureness.provider.Account; +import com.usthe.sureness.provider.AccountProvider; import com.usthe.sureness.subject.Subject; import com.usthe.sureness.subject.SubjectAuToken; import com.usthe.sureness.subject.support.PasswordSubjectToken; +import com.usthe.sureness.util.Md5Util; /** * 支持 username password 类型token的处理器实例 @@ -12,6 +19,9 @@ import com.usthe.sureness.subject.support.PasswordSubjectToken; */ public class PasswordProcessor extends BaseProcessor { + + private AccountProvider accountProvider; + @Override public boolean canSupportAuTokenClass(Class var) { return var != null && var == PasswordSubjectToken.class; @@ -25,13 +35,22 @@ public class PasswordProcessor extends BaseProcessor { } @Override - public boolean authenticated(SubjectAuToken var) { - return false; + public boolean authenticated(SubjectAuToken var) throws SurenessAuthenticationException { + String appId = (String) var.getPrincipal(); + Account account = accountProvider.loadAccount(appId); + if (account == null) { + throw new UnknownAccountException("no the account: " + appId); + } + String password = Md5Util.md5((String) var.getCredentials() + account.getSalt()); + if (password == null || !password.equals(account.getPassword())) { + throw new IncorrectCredentialsException("incorrect password"); + } + return true; } @Override - public boolean authorized(SubjectAuToken var) { - return false; + public boolean authorized(SubjectAuToken var) throws SurenessAuthorizationException { + return true; } @Override diff --git a/core/src/main/java/com/usthe/sureness/provider/Account.java b/core/src/main/java/com/usthe/sureness/provider/Account.java new file mode 100644 index 0000000000000000000000000000000000000000..c817fd378a4c3fbab4f24da915bc9fb5196d8a9e --- /dev/null +++ b/core/src/main/java/com/usthe/sureness/provider/Account.java @@ -0,0 +1,18 @@ +package com.usthe.sureness.provider; + + +/** + * @author tomsun28 + * @date 23:18 2019-04-02 + */ +public interface Account { + + + String getAppId(); + + String getPassword(); + + String getSalt(); + +} + diff --git a/core/src/main/java/com/usthe/sureness/provider/AccountProvider.java b/core/src/main/java/com/usthe/sureness/provider/AccountProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..a0e7fc033f614a38ee48a157d0d8bba55f949c2c --- /dev/null +++ b/core/src/main/java/com/usthe/sureness/provider/AccountProvider.java @@ -0,0 +1,11 @@ +package com.usthe.sureness.provider; + +/** + * @author tomsun28 + * @date 23:02 2019-04-02 + */ +public interface AccountProvider { + + Account loadAccount(String appId); + +} diff --git a/core/src/main/java/com/usthe/sureness/subject/support/DefaultSubject.java b/core/src/main/java/com/usthe/sureness/subject/support/DefaultSubject.java index f27d36c020862aad8da8dbd87b8c25a3b5c06f35..b06a6d619d6c62882e87aa6023b3a21ac4688982 100644 --- a/core/src/main/java/com/usthe/sureness/subject/support/DefaultSubject.java +++ b/core/src/main/java/com/usthe/sureness/subject/support/DefaultSubject.java @@ -61,7 +61,7 @@ public class DefaultSubject implements Subject { return new Builder(); } - private static class Builder { + public static class Builder { private String principal; private List roles; private String targetResource; diff --git a/core/src/main/java/com/usthe/sureness/subject/support/DefaultSubjectFactory.java b/core/src/main/java/com/usthe/sureness/subject/support/DefaultSubjectFactory.java index cea66b1990ba088af551d85ba82cbdad460ba0cb..2df49edbbd7d3c593a03330687214f1afc4b4d5f 100644 --- a/core/src/main/java/com/usthe/sureness/subject/support/DefaultSubjectFactory.java +++ b/core/src/main/java/com/usthe/sureness/subject/support/DefaultSubjectFactory.java @@ -32,8 +32,11 @@ public class DefaultSubjectFactory implements SubjectFactory { String principal = (String)auToken.getPrincipal(); List roles = (List)auToken.getOwnRoles(); String targetUri = (String)auToken.getTargetResource(); - Subject subject = new DefaultSubject().setPrincipal(principal) - .setRoles(roles).setTargetResource(targetUri); + Subject subject = DefaultSubject.getBuilder() + .setTargetResource(targetUri) + .addRoles(roles) + .setPrincipal(principal) + .build(); // 将subject 绑定到localThread变量中 ThreadContext.bind(subject); // 如果是网关认证中心, 之后可以考虑把subject绑定到request请求中,供子系统使用 @@ -61,4 +64,5 @@ public class DefaultSubjectFactory implements SubjectFactory { throw new SurenessNoInitException("the subjectFactory not complete ye"); } } + } diff --git a/core/src/main/java/com/usthe/sureness/subject/support/JwtSubjectToken.java b/core/src/main/java/com/usthe/sureness/subject/support/JwtSubjectToken.java index 8b6e09d0d5a0ed14d4168cc058e5f55efe043c45..713b41c992099fbc0ca28f87cae2a5cbc6a02428 100644 --- a/core/src/main/java/com/usthe/sureness/subject/support/JwtSubjectToken.java +++ b/core/src/main/java/com/usthe/sureness/subject/support/JwtSubjectToken.java @@ -34,7 +34,11 @@ public class JwtSubjectToken implements SubjectAuToken { */ private List roles; /** - * + * 所访问资源地址 + */ + private String targetUri; + /** + * 所访问资源他支持的角色 */ private List supportRoles; @@ -50,21 +54,21 @@ public class JwtSubjectToken implements SubjectAuToken { @Override public Object getOwnRoles() { - return null; + return this.roles; } @Override public Object getTargetResource() { - return null; + return this.targetUri; } @Override public Object getSupportRoles() { - return null; + return this.supportRoles; } @Override public void setSupportRoles(Object var1) { - + this.supportRoles = (List) var1; } } diff --git a/core/src/main/java/com/usthe/sureness/subject/support/PasswordSubjectToken.java b/core/src/main/java/com/usthe/sureness/subject/support/PasswordSubjectToken.java index 9da2eb8e4738b89912e0aff017b426b23b297a72..135a2150e9d147999f1792193bc20bf20589f195 100644 --- a/core/src/main/java/com/usthe/sureness/subject/support/PasswordSubjectToken.java +++ b/core/src/main/java/com/usthe/sureness/subject/support/PasswordSubjectToken.java @@ -13,40 +13,38 @@ public class PasswordSubjectToken implements SubjectAuToken { private static final long serialVersionUID = 1L; private String appId; private String password; - private String timestamp; private String host; - private String tokenKey; + private String targetUri; private List roles; private List supportRoles; @Override public Object getPrincipal() { - return null; + return this.appId; } @Override public Object getCredentials() { - return null; + return this.password; } @Override public Object getOwnRoles() { - return null; + return this.roles; } @Override public Object getTargetResource() { - return null; + return this.targetUri; } @Override public Object getSupportRoles() { - return null; + return supportRoles; } @Override public void setSupportRoles(Object var1) { - } } diff --git a/core/src/main/java/com/usthe/sureness/util/JsonWebTokenUtil.java b/core/src/main/java/com/usthe/sureness/util/JsonWebTokenUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..6d61246d013254359cf3c6fff8719dfe5161c18a --- /dev/null +++ b/core/src/main/java/com/usthe/sureness/util/JsonWebTokenUtil.java @@ -0,0 +1,199 @@ +package com.usthe.sureness.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.*; +import io.jsonwebtoken.impl.DefaultHeader; +import io.jsonwebtoken.impl.DefaultJwsHeader; +import io.jsonwebtoken.impl.TextCodec; +import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; +import io.jsonwebtoken.lang.Assert; + + +import javax.xml.bind.DatatypeConverter; +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author tomsun28 + * @date 16:29 2018/3/8 + */ +public class JsonWebTokenUtil { + + public static final String SECRET_KEY = "?::4343fdf4fdf6cvf):"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final int COUNT_2 = 2; + private static CompressionCodecResolver codecResolver = new DefaultCompressionCodecResolver(); + + private JsonWebTokenUtil() { + + } + + /** + * json web token 签发 + * @param id 令牌ID + * @param subject 用户ID + * @param issuer 签发人 + * @param period 有效时间(毫秒) + * @param roles 访问主张-角色 + * @param permissions 访问主张-权限 + * @param algorithm 加密算法 + * @return java.lang.String + */ + public static String issueJWT(String id, String subject, String issuer, Long period, String roles, String permissions, SignatureAlgorithm algorithm) { + // 当前时间戳 + Long currentTimeMillis = System.currentTimeMillis(); + // 秘钥 + byte[] secreKeyBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY); + JwtBuilder jwtBuilder = Jwts.builder(); + if (id != null) { + jwtBuilder.setId(id); + } + if (subject != null) { + jwtBuilder.setSubject(subject); + } + if (issuer != null) { + jwtBuilder.setIssuer(issuer); + } + // 设置签发时间 + jwtBuilder.setIssuedAt(new Date(currentTimeMillis)); + // 设置到期时间 + if (null != period) { + jwtBuilder.setExpiration(new Date(currentTimeMillis+period*1000)); + } + if (roles != null) { + jwtBuilder.claim("roles", roles); + } + if (permissions != null) { + jwtBuilder.claim("perms", permissions); + } + // 压缩,可选GZIP + jwtBuilder.compressWith(CompressionCodecs.DEFLATE); + // 加密设置 + jwtBuilder.signWith(algorithm,secreKeyBytes); + return jwtBuilder.compact(); + } + + /** + * 解析JWT的Payload + */ + public static String parseJwtPayload(String jwt){ + Assert.hasText(jwt, "JWT String argument cannot be null or empty."); + String base64UrlEncodedHeader = null; + String base64UrlEncodedPayload = null; + String base64UrlEncodedDigest = null; + int delimiterCount = 0; + StringBuilder sb = new StringBuilder(128); + for (char c : jwt.toCharArray()) { + if (c == '.') { + CharSequence tokenSeq = io.jsonwebtoken.lang.Strings.clean(sb); + String token = tokenSeq!=null?tokenSeq.toString():null; + + if (delimiterCount == 0) { + base64UrlEncodedHeader = token; + } else if (delimiterCount == 1) { + base64UrlEncodedPayload = token; + } + + delimiterCount++; + sb.setLength(0); + } else { + sb.append(c); + } + } + if (delimiterCount != COUNT_2) { + String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount; + throw new MalformedJwtException(msg); + } + if (sb.length() > 0) { + base64UrlEncodedDigest = sb.toString(); + } + if (base64UrlEncodedPayload == null) { + throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload."); + } + // =============== Header ================= + Header header = null; + CompressionCodec compressionCodec = null; + if (base64UrlEncodedHeader != null) { + String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader); + Map m = readValue(origValue); + if (base64UrlEncodedDigest != null) { + header = new DefaultJwsHeader(m); + } else { + header = new DefaultHeader(m); + } + compressionCodec = codecResolver.resolveCompressionCodec(header); + } + // =============== Body ================= + String payload; + if (compressionCodec != null) { + byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload)); + payload = new String(decompressed, io.jsonwebtoken.lang.Strings.UTF_8); + } else { + payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload); + } + return payload; + } + + /** + * 验签JWT + * + * @param jwt json web token + */ + public static Claims parseJwt(String jwt, String appKey) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { + return Jwts.parser() + .setSigningKey(DatatypeConverter.parseBase64Binary(appKey)) + .parseClaimsJws(jwt) + .getBody(); + +// JwtAccount jwtAccount = new JwtAccount(); +// //令牌ID +// jwtAccount.setTokenId(claims.getId()); +// // 客户标识 +// jwtAccount.setAppId(claims.getSubject()); +// // 签发者 +// jwtAccount.setIssuer(claims.getIssuer()); +// // 签发时间 +// jwtAccount.setIssuedAt(claims.getIssuedAt()); +// // 接收方 +// jwtAccount.setAudience(claims.getAudience()); +// // 访问主张-角色 +// jwtAccount.setRoles(claims.get("roles", String.class)); +// // 访问主张-权限 +// jwtAccount.setPerms(claims.get("perms", String.class)); + } + + + /** + * description 从json数据中读取格式化map + * + * @param val 1 + * @return java.util.Map + */ + @SuppressWarnings("unchecked") + public static Map readValue(String val) { + try { + return MAPPER.readValue(val, Map.class); + } catch (IOException e) { + throw new MalformedJwtException("Unable to read JSON value: " + val, e); + } + } + + /** + * 分割字符串进SET + */ + @SuppressWarnings("unchecked") + public static Set split(String str) { + + Set set = new HashSet<>(); + if (str == null || "".equals(str)) { + return set; + } + Arrays.asList(str.split(",")); + return set; + } + +} diff --git a/core/src/main/java/com/usthe/sureness/util/Md5Util.java b/core/src/main/java/com/usthe/sureness/util/Md5Util.java new file mode 100644 index 0000000000000000000000000000000000000000..80a50cc6f6d038b04d6a6382162e55b29a69167a --- /dev/null +++ b/core/src/main/java/com/usthe/sureness/util/Md5Util.java @@ -0,0 +1,51 @@ +package com.usthe.sureness.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +/** + * @author tomsun28 + * @date 20:48 2018/2/27 + */ +public class Md5Util { + + private static final Logger LOGGER = LoggerFactory.getLogger(Md5Util.class); + + public static String md5(String content) { + // 用于加密的字符 + char[] md5String = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; + try { + // 使用平台默认的字符集将md5String编码为byte序列,并将结果存储到一个新的byte数组中 + byte[] byteInput = content.getBytes(StandardCharsets.UTF_8); + + // 信息摘要是安全的单向哈希函数,它接收任意大小的数据,并输出固定长度的哈希值 + MessageDigest mdInst = MessageDigest.getInstance("MD5"); + + // MessageDigest对象通过使用update方法处理数据,使用指定的byte数组更新摘要 + mdInst.update(byteInput); + + //摘要更新后通过调用digest() 执行哈希计算,获得密文 + byte[] md = mdInst.digest(); + + //把密文转换成16进制的字符串形式 + int j = md.length; + char[] str = new char[j*2]; + int k = 0; + for (int i=0; i>> 4 & 0xf]; + str[k++] = md5String[byte0 & 0xf]; + } + // 返回加密后的字符串 + return new String(str); + + }catch (Exception e) { + LOGGER.warn(e.getMessage(),e); + return null; + } + + } +}