未验证 提交 0a2d12ed 编写于 作者: sinat_25235033's avatar sinat_25235033 提交者: GitHub

Merge pull request #29 from tomsun28/digest-auth-feature

Digest auth feature - support digest auth with servlet and jax-rs
......@@ -4,6 +4,7 @@ import com.usthe.sureness.matcher.DefaultPathRoleMatcher;
import com.usthe.sureness.mgt.SurenessSecurityManager;
import com.usthe.sureness.processor.DefaultProcessorManager;
import com.usthe.sureness.processor.Processor;
import com.usthe.sureness.processor.support.DigestProcessor;
import com.usthe.sureness.processor.support.JwtProcessor;
import com.usthe.sureness.processor.support.NoneProcessor;
import com.usthe.sureness.processor.support.PasswordProcessor;
......@@ -11,12 +12,7 @@ import com.usthe.sureness.provider.ducument.DocumentResourceDefaultProvider;
import com.usthe.sureness.subject.SubjectCreate;
import com.usthe.sureness.subject.SubjectFactory;
import com.usthe.sureness.subject.SurenessSubjectFactory;
import com.usthe.sureness.subject.creater.BasicSubjectJaxRsCreator;
import com.usthe.sureness.subject.creater.JwtSubjectJaxRsCreator;
import com.usthe.sureness.subject.creater.JwtSubjectServletCreator;
import com.usthe.sureness.subject.creater.BasicSubjectServletCreator;
import com.usthe.sureness.subject.creater.NoneSubjectJaxRsCreator;
import com.usthe.sureness.subject.creater.NoneSubjectServletCreator;
import com.usthe.sureness.subject.creater.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -60,6 +56,9 @@ public class DefaultSurenessConfig {
PasswordProcessor passwordProcessor = new PasswordProcessor();
passwordProcessor.setAccountProvider(resourceProvider);
processorList.add(passwordProcessor);
DigestProcessor digestProcessor = new DigestProcessor();
digestProcessor.setAccountProvider(resourceProvider);
processorList.add(digestProcessor);
DefaultProcessorManager processorManager = new DefaultProcessorManager(processorList);
if (logger.isDebugEnabled()) {
logger.debug("DefaultProcessorManager init");
......@@ -80,12 +79,14 @@ public class DefaultSurenessConfig {
subjectCreates = Arrays.asList(
new NoneSubjectJaxRsCreator(),
new BasicSubjectJaxRsCreator(),
new JwtSubjectJaxRsCreator());
new JwtSubjectJaxRsCreator(),
new DigestSubjectJaxRsCreator());
} else {
subjectCreates = Arrays.asList(
new NoneSubjectServletCreator(),
new BasicSubjectServletCreator(),
new JwtSubjectServletCreator());
new JwtSubjectServletCreator(),
new DigestSubjectServletCreator());
}
subjectFactory.registerSubjectCreator(subjectCreates);
if (logger.isDebugEnabled()) {
......
package com.usthe.sureness.processor.exception;
/**
* exception for digest auth
* means you should try once again by authenticate below with digest auth information
* @author tomsun28
* @date 2020-10-28 23:27
*/
public class NeedDigestInfoException extends SurenessAuthenticationException {
private String authenticate;
public NeedDigestInfoException(String message, String authenticate) {
super(message);
this.authenticate = authenticate;
}
public String getAuthenticate() {
return authenticate;
}
}
package com.usthe.sureness.processor.support;
import com.usthe.sureness.processor.BaseProcessor;
import com.usthe.sureness.processor.exception.*;
import com.usthe.sureness.provider.SurenessAccount;
import com.usthe.sureness.provider.SurenessAccountProvider;
import com.usthe.sureness.subject.Subject;
import com.usthe.sureness.subject.support.DigestSubject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
/**
* process digest auth
* @author tomsun28
* @date 2020-10-28 23:17
*/
public class DigestProcessor extends BaseProcessor {
private static final Logger logger = LoggerFactory.getLogger(DigestProcessor.class);
private static final String DEFAULT_REALM = "sureness_realm";
private static final String DEFAULT_QOP = "auth";
private static final String HEX_LOOKUP = "0123456789abcdef";
private static MessageDigest md5Digest;
private static String realm;
private static String qop;
private SurenessAccountProvider accountProvider;
static {
try {
md5Digest = MessageDigest.getInstance("MD5");
realm = DEFAULT_REALM;
qop = DEFAULT_QOP;
} catch (NoSuchAlgorithmException e) {
logger.error(e.getMessage(), e);
}
}
@Override
public boolean canSupportAuTokenClass(Class<?> var) {
return var == DigestSubject.class;
}
@Override
public Class<?> getSupportAuTokenClass() {
return DigestSubject.class;
}
@Override
public Subject authenticated(Subject var) throws SurenessAuthenticationException {
if (var.getPrincipal() == null || var.getCredentials() == null) {
String authenticate = getAuthenticate();
throw new NeedDigestInfoException("you should try once with digest auth information", authenticate);
}
String appId = (String) var.getPrincipal();
SurenessAccount account = accountProvider.loadAccount(appId);
if (account == null) {
if (logger.isDebugEnabled()) {
logger.debug("PasswordProcessor authenticated fail, no this user: {}",
var.getPrincipal());
}
throw new UnknownAccountException("do not exist the account: " + appId);
}
DigestSubject digestSubject = (DigestSubject) var;
//A1 = MD5("username:realm:password");
String a1 = calcDigest(appId, digestSubject.getRealm(), account.getPassword());
//A2 = MD5("httpMethod:uri");
String a2 = calcDigest(digestSubject.getHttpMethod(), digestSubject.getUri());
//response = MD5("A1:nonce:nc:cNonce:qop:A2");
String oriResponse = calcDigest(a1, digestSubject.getNonce(), digestSubject.getNc(), digestSubject.getCnonce(),
digestSubject.getQop(), a2);
if (!oriResponse.equals(digestSubject.getCredentials())) {
throw new IncorrectCredentialsException("incorrect password");
}
if (account.isDisabledAccount()) {
throw new DisabledAccountException("account is disabled");
}
if (account.isExcessiveAttempts()) {
throw new ExcessiveAttemptsException("account is disable due to many time authenticated, try later");
}
return DigestSubject.builder(var)
.setOwnRoles(account.getOwnRoles())
.build();
}
@SuppressWarnings("unchecked")
@Override
public void authorized(Subject var) throws SurenessAuthorizationException {
List<String> ownRoles = (List<String>)var.getOwnRoles();
List<String> supportRoles = (List<String>)var.getSupportRoles();
if (supportRoles == null || supportRoles.isEmpty() || supportRoles.stream().anyMatch(ownRoles::contains)) {
return;
}
throw new UnauthorizedException("do not have the role to access resource");
}
private String getAuthenticate(){
String nonce = calcDigest(String.valueOf(System.currentTimeMillis()));
return "Digest " + "realm=" + realm + ",nonce=" + nonce + ",qop=" + qop;
}
private String calcDigest(String first, String ... args){
StringBuilder stringBuilder = new StringBuilder(first);
if (args != null) {
for (String str : args){
stringBuilder.append(':').append(str);
}
}
md5Digest.reset();
md5Digest.update(stringBuilder.toString().getBytes(StandardCharsets.UTF_8));
return bytesToHexString(md5Digest.digest());
}
private static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte aByte : bytes) {
sb.append(HEX_LOOKUP.charAt((aByte & 0xF0) >> 4));
sb.append(HEX_LOOKUP.charAt((aByte & 0x0F)));
}
return sb.toString();
}
public void setAccountProvider(SurenessAccountProvider provider) {
this.accountProvider = provider;
}
public static void setRealm(String realm) {
DigestProcessor.realm = realm;
}
public static void setQop(String qop) {
DigestProcessor.qop = qop;
}
}
package com.usthe.sureness.subject.creater;
import com.usthe.sureness.subject.Subject;
import com.usthe.sureness.subject.SubjectCreate;
import com.usthe.sureness.subject.support.DigestSubject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.container.ContainerRequestContext;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* digest subject creator
* @author tomsun28
* @date 2020-10-28 20:44
*/
public class DigestSubjectJaxRsCreator implements SubjectCreate {
private static final Logger logger = LoggerFactory.getLogger(DigestSubjectJaxRsCreator.class);
private static final String AUTHORIZATION = "Authorization";
private static final String DIGEST = "Digest ";
private static final String USERNAME = "username";
private static final String NONCE = "nonce";
private static final String QOP = "qop";
private static final String REALM = "realm";
private static final String NC = "nc";
private static final String CNONCE = "cnonce";
private static final String RESPONSE = "response";
private static final String URI = "uri";
private static final int FILED_SIZE = 2;
private static final String SPLIT = "\"";
@Override
public boolean canSupportSubject(Object context) {
return context instanceof ContainerRequestContext;
}
@Override
public Subject createSubject(Object context) {
String authorization = ((ContainerRequestContext)context).getHeaderString(AUTHORIZATION);
if (authorization == null || !authorization.startsWith(DIGEST)) {
return new DigestSubject();
} else {
// digest auth
String digestAuth = authorization.replace(DIGEST, "").trim();
try {
final Map<String, String> digestMap = new HashMap<>(8);
Arrays.stream(digestAuth.split(",")).forEach(auth -> {
String[] tmpArr = auth.trim().split("=");
if (tmpArr.length == FILED_SIZE) {
String authValue = tmpArr[1].trim();
if (authValue.startsWith(SPLIT) && authValue.endsWith(SPLIT)) {
authValue = authValue.substring(1, authValue.length() - 1);
}
digestMap.put(tmpArr[0].trim(), authValue);
}
});
String username = digestMap.get(USERNAME);
String response = digestMap.get(RESPONSE);
String realm = digestMap.get(REALM);
String uri = digestMap.get(URI);
String nonce = digestMap.get(NONCE);
String nc = digestMap.get(NC);
String cNonce = digestMap.get(CNONCE);
String qop = digestMap.get(QOP);
if (username == null || response == null || realm == null || uri == null
|| nonce == null || nc == null || cNonce == null) {
logger.debug("can not create digest subject due some need field is null");
return null;
}
String requestUri = ((ContainerRequestContext) context).getUriInfo().getPath();
String requestType = ((ContainerRequestContext) context).getMethod();
String targetUri = requestUri.concat("===").concat(requestType).toLowerCase();
return DigestSubject.builder(username, response)
.setRealm(realm).setUri(uri).setNonce(nonce)
.setNc(nc).setCnonce(cNonce).setQop(qop)
.setHttpMethod(requestType.toUpperCase())
.setTargetUri(targetUri)
.build();
} catch (Exception e) {
logger.info(e.getMessage(), e);
return null;
}
}
}
}
package com.usthe.sureness.subject.creater;
import com.usthe.sureness.subject.Subject;
import com.usthe.sureness.subject.SubjectCreate;
import com.usthe.sureness.subject.support.DigestSubject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* digest subject creator
* @author tomsun28
* @date 2020-10-28 20:44
*/
public class DigestSubjectServletCreator implements SubjectCreate {
private static final Logger logger = LoggerFactory.getLogger(DigestSubjectServletCreator.class);
private static final String AUTHORIZATION = "Authorization";
private static final String DIGEST = "Digest ";
private static final String USERNAME = "username";
private static final String NONCE = "nonce";
private static final String QOP = "qop";
private static final String REALM = "realm";
private static final String NC = "nc";
private static final String CNONCE = "cnonce";
private static final String RESPONSE = "response";
private static final String URI = "uri";
private static final int FILED_SIZE = 2;
private static final String SPLIT = "\"";
@Override
public boolean canSupportSubject(Object context) {
return context instanceof HttpServletRequest;
}
@Override
public Subject createSubject(Object context) {
String authorization = ((HttpServletRequest)context).getHeader(AUTHORIZATION);
if (authorization == null || !authorization.startsWith(DIGEST)) {
return new DigestSubject();
} else {
// digest auth
String digestAuth = authorization.replace(DIGEST, "").trim();
try {
final Map<String, String> digestMap = new HashMap<>(8);
Arrays.stream(digestAuth.split(",")).forEach(auth -> {
String[] tmpArr = auth.trim().split("=");
if (tmpArr.length == FILED_SIZE) {
String authValue = tmpArr[1].trim();
if (authValue.startsWith(SPLIT) && authValue.endsWith(SPLIT)) {
authValue = authValue.substring(1, authValue.length() - 1);
}
digestMap.put(tmpArr[0].trim(), authValue);
}
});
String username = digestMap.get(USERNAME);
String response = digestMap.get(RESPONSE);
String realm = digestMap.get(REALM);
String uri = digestMap.get(URI);
String nonce = digestMap.get(NONCE);
String nc = digestMap.get(NC);
String cNonce = digestMap.get(CNONCE);
String qop = digestMap.get(QOP);
if (username == null || response == null || realm == null || uri == null
|| nonce == null || nc == null || cNonce == null) {
logger.debug("can not create digest subject due some need field is null");
return null;
}
String remoteHost = ((HttpServletRequest) context).getRemoteHost();
String requestUri = ((HttpServletRequest) context).getRequestURI();
String requestType = ((HttpServletRequest) context).getMethod();
String targetUri = requestUri.concat("===").concat(requestType).toLowerCase();
return DigestSubject.builder(username, response)
.setRealm(realm).setUri(uri).setNonce(nonce)
.setNc(nc).setCnonce(cNonce).setQop(qop).setHttpMethod(requestType.toUpperCase())
.setRemoteHost(remoteHost).setTargetUri(targetUri)
.build();
} catch (Exception e) {
logger.info(e.getMessage(), e);
return null;
}
}
}
}
package com.usthe.sureness.subject.support;
import com.usthe.sureness.subject.Subject;
import java.util.List;
/**
* subject for digest auth
* @author tomsun28
* @date 2020-10-28 20:44
*/
public class DigestSubject implements Subject {
private static final long serialVersionUID = 1L;
/** 用户标识 **/
private String appId;
/** 安全域 **/
private String realm;
/** uri **/
private String uri;
/** 保护质量,包含auth(默认的)和 auth-int **/
private String qop;
/** 服务端向客户端发送质询时附带的一个随机数 **/
private String nonce;
/** nonce计数器,是一个16进制的数值 **/
private String nc;
/** 客户端随机数 **/
private String cnonce;
/** 加密后的口令 **/
private String response;
/** 请求的http method **/
private String httpMethod;
/** 访问用户的IP **/
private String remoteHost;
/** 所拥有的角色 在解析完jwt之后把用户角色放到这里 **/
private List<String> ownRoles;
/** 所访问资源地址 **/
private String targetUri;
/** 所访问资源他支持的角色 **/
private List<String> supportRoles;
public DigestSubject() {}
private DigestSubject(Builder builder) {
this.appId = builder.appId;
this.response = builder.response;
this.realm = builder.realm;
this.uri = builder.uri;
this.qop = builder.qop;
this.nonce = builder.nonce;
this.nc = builder.nc;
this.cnonce = builder.cnonce;
this.httpMethod = builder.httpMethod;
this.remoteHost = builder.remoteHost;
this.ownRoles = builder.ownRoles;
this.targetUri = builder.targetUri;
this.supportRoles = builder.supportRoles;
}
@Override
public Object getPrincipal() {
return appId;
}
@Override
public Object getCredentials() {
return response;
}
@Override
public Object getOwnRoles() {
return ownRoles;
}
@Override
public Object getTargetResource() {
return targetUri;
}
@Override
public Object getSupportRoles() {
return supportRoles;
}
@SuppressWarnings("unchecked")
@Override
public void setSupportRoles(Object var1) {
this.supportRoles = (List<String>) var1;
}
public String getRealm() {
return realm;
}
public String getUri() {
return uri;
}
public String getQop() {
return qop;
}
public String getNonce() {
return nonce;
}
public String getNc() {
return nc;
}
public String getCnonce() {
return cnonce;
}
public String getHttpMethod() {
return httpMethod;
}
public String getRemoteHost() {
return remoteHost;
}
public static DigestSubject.Builder builder(String username, String response) {
return new DigestSubject.Builder(username, response);
}
public static DigestSubject.Builder builder(Subject auToken) {
return new DigestSubject.Builder(auToken);
}
public static class Builder {
private String appId;
private String response;
private String realm;
private String uri;
private String qop;
private String nonce;
private String nc;
private String cnonce;
private String httpMethod;
private String remoteHost;
private List<String> ownRoles;
private String targetUri;
private List<String> supportRoles;
public Builder(String username, String response) {
this.appId = username;
this.response = response;
}
@SuppressWarnings("unchecked")
public Builder(Subject auToken) {
this.appId = String.valueOf(auToken.getPrincipal());
this.response = String.valueOf(auToken.getCredentials());
this.ownRoles = (List<String>) auToken.getOwnRoles();
this.targetUri = String.valueOf(auToken.getTargetResource());
this.supportRoles = (List<String>) auToken.getSupportRoles();
}
public DigestSubject.Builder setAppId(String appId) {
this.appId = appId;
return this;
}
public DigestSubject.Builder setResponse(String response) {
this.response = response;
return this;
}
public DigestSubject.Builder setRealm(String realm) {
this.realm = realm;
return this;
}
public DigestSubject.Builder setUri(String uri) {
this.uri = uri;
return this;
}
public DigestSubject.Builder setQop(String qop) {
this.qop = qop;
return this;
}
public DigestSubject.Builder setNonce(String nonce) {
this.nonce = nonce;
return this;
}
public DigestSubject.Builder setNc(String nc) {
this.nc = nc;
return this;
}
public DigestSubject.Builder setCnonce(String cnonce) {
this.cnonce = cnonce;
return this;
}
public DigestSubject.Builder setHttpMethod(String httpMethod) {
this.httpMethod = httpMethod;
return this;
}
public DigestSubject.Builder setTargetUri(String targetUri) {
this.targetUri = targetUri;
return this;
}
public DigestSubject.Builder setRemoteHost(String remoteHost) {
this.remoteHost = remoteHost;
return this;
}
public DigestSubject.Builder setOwnRoles(List<String> ownRoles) {
this.ownRoles = ownRoles;
return this;
}
public DigestSubject.Builder setSupportRoles(List<String> supportRoles) {
this.supportRoles = supportRoles;
return this;
}
public DigestSubject build() {
return new DigestSubject(this);
}
}
}
......@@ -32,6 +32,7 @@ excludedResource:
account:
- appId: admin
# if add salt, the password is encrypted password - the result: MD5(password+salt)
# digest auth not support encrypted password
# if no salt, the password is unencrypted password
credential: 0192023A7BBD73250516F069DF18B500
salt: 123
......
package com.usthe.sureness.sample.bootstrap;
import com.usthe.sureness.mgt.SurenessSecurityManager;
import com.usthe.sureness.processor.exception.DisabledAccountException;
import com.usthe.sureness.processor.exception.ExcessiveAttemptsException;
import com.usthe.sureness.processor.exception.ExpiredCredentialsException;
import com.usthe.sureness.processor.exception.IncorrectCredentialsException;
import com.usthe.sureness.processor.exception.ProcessorNotFoundException;
import com.usthe.sureness.processor.exception.UnauthorizedException;
import com.usthe.sureness.processor.exception.UnknownAccountException;
import com.usthe.sureness.processor.exception.UnsupportedSubjectException;
import com.usthe.sureness.processor.exception.*;
import com.usthe.sureness.sample.bootstrap.util.CommonUtil;
import com.usthe.sureness.subject.SubjectSum;
import com.usthe.sureness.util.SurenessContextHolder;
......@@ -67,17 +60,22 @@ public class SurenessFilterExample implements Filter {
} catch (DisabledAccountException | ExcessiveAttemptsException e2 ) {
logger.debug("the account is disabled");
CommonUtil.responseWrite(ResponseEntity
.status(HttpStatus.FORBIDDEN).body(e2.getMessage()), servletResponse);
.status(HttpStatus.UNAUTHORIZED).body(e2.getMessage()), servletResponse);
return;
} catch (IncorrectCredentialsException | ExpiredCredentialsException e3) {
logger.debug("this account credential is incorrect or expired");
CommonUtil.responseWrite(ResponseEntity
.status(HttpStatus.FORBIDDEN).body(e3.getMessage()), servletResponse);
.status(HttpStatus.UNAUTHORIZED).body(e3.getMessage()), servletResponse);
return;
} catch (UnauthorizedException e5) {
} catch (NeedDigestInfoException e5) {
logger.debug("you should try once again with digest auth information");
CommonUtil.responseWrite(ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.header("WWW-Authenticate", e5.getAuthenticate()).build(), servletResponse);
} catch (UnauthorizedException e6) {
logger.debug("this account can not access this resource");
CommonUtil.responseWrite(ResponseEntity
.status(HttpStatus.FORBIDDEN).body(e5.getMessage()), servletResponse);
.status(HttpStatus.FORBIDDEN).body(e6.getMessage()), servletResponse);
return;
} catch (RuntimeException e) {
logger.error("other exception happen: ", e);
......
......@@ -32,6 +32,7 @@ excludedResource:
account:
- appId: admin
# if add salt, the password is encrypted password - the result: MD5(password+salt)
# digest auth not support encrypted password
# if no salt, the password is unencrypted password
credential: 0192023A7BBD73250516F069DF18B500
salt: 123
......
......@@ -32,6 +32,7 @@ excludedResource:
account:
- appId: admin
# if add salt, the password is encrypted password - the result: MD5(password+salt)
# digest auth not support encrypted password
# if no salt, the password is unencrypted password
credential: 0192023A7BBD73250516F069DF18B500
salt: 123
......
......@@ -32,6 +32,7 @@ excludedResource:
account:
- appId: admin
# if add salt, the password is encrypted password - the result: MD5(password+salt)
# digest auth not support encrypted password
# if no salt, the password is unencrypted password
credential: 0192023A7BBD73250516F069DF18B500
salt: 123
......
......@@ -32,6 +32,7 @@ excludedResource:
account:
- appId: admin
# if add salt, the password is encrypted password - the result: MD5(password+salt)
# digest auth not support encrypted password
# if no salt, the password is unencrypted password
credential: 0192023A7BBD73250516F069DF18B500
salt: 123
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册