介绍
REST对于资源型服务接口来说很合适,同时特别适合对于效率要求很高,但是对于安全要求不高的场景
具体做法是:
- 在每一个REST的call的头header(例如:@HeaderParam)都带user token 和 application ID
- 在服务器端对每一请求进行re-authentication(可以写一个拦截器来实现)
- SpringSecurity 配置
create-session="stateless"
- 这样每个请求都是无状态的独立的,需要被再次认证re-authentication
- SpringSecurity 配置
- 注意:把token放在uri中是糟糕的做法
- 首先是安全的原因
- 其次是cache的原因,尽量放在head中
//RestTemplate默认使用的HttpClient未设置凭证,stateless请求
RestTemplate rt=new RestTemplate();
HttpClient
认证方案(authentication schemes)
Basic/Digest/NTLM 这些方案可用于服务器或代理对客户端的认证
Basic
基本认证: 以明码传送username和password(最不安全的一个方案),一般使用 Basic +TLS/SSL 加密方式。[使用UsernamePasswordCredentials
实例]Digest
摘要式认证: 传送用password对Server端发送的随机数(nonce)加密后生成的字符串 (比Basic安全) [使用UsernamePasswordCredentials
实例]NTLM
最复杂,认证了一个连接而不是一请求 [使用NTCredentials
实例]- 自定义
用户凭证(Credentials)
- 任何用户身份验证的过程都需要一组可以用于建立用户身份的凭据
- 用户凭证的最简单的形式可以仅仅是用户名/密码对
UsernamePasswordCredentials
代表了一组包含安全规则和明文密码的凭据UsernamePasswordCredentials creds = new UsernamePasswordCredentials("user", "pwd"); System.out.println(creds.getUserPrincipal().getName()); System.out.println(creds.getPassword());
NTCredentials
是微软Windows指定的实现,它包含了除了用户名/密码对外,一组额外的Windows指定的属性,比如用户域名的名字(相同的用户使用不同设置的认证可以属于不同的域)NTCredentials creds = new NTCredentials("user", "pwd", "workstation", "domain"); System.out.println(creds.getUserPrincipal().getName()); System.out.println(creds.getPassword());
- 凭据提供器
CredentialsProvider
- 用来维护一组用户凭据,还有能够对特定认证范围
AuthScope
产生用户凭据Credential
- 认证范围包括:主机名
Hostname
,端口号port
,领域名称authScope
,访问空间realm
,认证方案authScheme
- 当使用凭据提供器来注册凭据时,可以提供一个通配符(任意主机,任意端口,任意领域,任意模式)来替代确定的属性值
- 如果直接匹配没有发现,
CredentialsProvider
期望被用来发现最匹配的特定范围 - eg:
CredentialsProvider credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials(new AuthScope("somehost", AuthScope.ANY_PORT),new UsernamePasswordCredentials("u1", "p1")); credsProvider.setCredentials(new AuthScope("somehost", 8080),new UsernamePasswordCredentials("u2", "p2")); credsProvider.setCredentials(new AuthScope("otherhost", 8080, AuthScope.ANY_REALM, "ntlm"),new UsernamePasswordCredentials("u3", "p3"));
- 用来维护一组用户凭据,还有能够对特定认证范围
认证Authentication
- 服务器认证(Server Authentication):HttpClient处理服务器认证几乎是透明的,仅需要开发人员提供登录信息(login credentials)
- 登录信息保存在HttpState类的实例中
- 可以通过
setCredentials(String realm, Credentials cred)和getCredentials(String realm)
来获取或设置 - 注意:设定对非特定站点访问所需要的登录信息,将realm参数置为null
- HttpClient内建的自动认证,可以通过HttpMethod类的
setDoAuthentication(boolean doAuthentication)
方法关闭(这次关闭只影响HttpMethod当前的实例) - 抢先认证(Preemptive Authentication)
- 在这种模式时,HttpClient会主动将basic认证应答信息传给服务器
- 即使在某种情况下服务器可能返回认证失败的应答
- 这样做主要是为了减少连接的建立
- 打开抢先认证:
client.getState().setAuthenticationPreemptive(true);
- 注意:
- 当需要与不被信任的站点或web应用通信时,应该谨慎使用缺省的认证机制
- 如果你提供的认证信息是敏感的,你应该指定认证域
AuthScope
,不推荐将认证域指定为AuthScope.ANY
- 代理认证(Proxy Authentication): 除了登录信息需单独存放以外,代理认证与服务器认证几乎一致
认证状态(AuthState)
用来跟踪关于认证过程状态的详细信息
- 在HTTP请求执行过程中,HttpClient创建2个
AuthState
的实例:- 一个对于目标主机认证
- 另外一个对于代理认证
- 如果目标服务器或代理需要用户认证,那么各自的
AuthState
实例将会被在认证处理过程中使用的AuthScope
,AuthScheme
和Crednetials
来填充。 AuthState
可以用来检查Request的认证类型,是否匹配AuthScheme
实现,CrendentialsProvider
是否对给定的AuthScope
匹配用户凭据- 在HTTP请求执行的过程中,HttpClient将下列和认证相关的对象添加到
HttpContext
:http.authscheme-registry
:AuthSchemeRegistry
实例代表真实的认证模式注册表http.auth.credentials-provider
:CookieSpec
实例代表了真实的凭据提供器http.auth.target-scope
:AuthState
实例代表了真实的目标认证状态http.auth.proxy-scope
:AuthState
实例代表了真实的代理认证状态
执行上下文(HttpContext)
- 实际上是HttpClient用来在多次请求-响应的交互中,保持状态信息用的
- 本地的HttpContext对象可以用于定制HTTP认证内容,并先于请求执行或在请求被执行之后检查它的状态
eg:
HttpContext localContext = new BasicHttpContext(); HttpResponse response = httpclient.execute(new HttpGet("http://localhost:8080/"), localContext); AuthState proxyAuthState = (AuthState) localContext.getAttribute(HttpClientContext.PROXY_AUTH_STATE); System.out.println("Proxy auth scope: " + proxyAuthState.getAuthScope()); System.out.println("Proxy auth scheme: " + proxyAuthState.getAuthScheme()); System.out.println("Proxy auth credentials: " + proxyAuthState.getCredentials()); AuthState targetAuthState = (AuthState) localContext.getAttribute(HttpClientContext.TARGET_AUTH_STATE); System.out.println("Target auth scope: " + targetAuthState.getAuthScope()); System.out.println("Target auth scheme: " + targetAuthState.getAuthScheme()); System.out.println("Target auth credentials: " + targetAuthState.getCredentials());
Client端实现方案
构建BasicAuthentication
在HttpHeader中明文发送Authentication,建议组合SSL/TLS加密使用
Authorization: "Basic "+new String( Base64.encodeBase64((username:password).getBytes()) )
方案一:每次提交Request时,构建Authorization,放入HttpHeader
中一并提交到Server
/*HttpClient*/
HttpClientBuilder builder=HttpClientBuilder.create();
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).useTLS().build();
SSLConnectionSocketFactory connectionFactory = new SSLConnectionSocketFactory(sslContext, new AllowAllHostnameVerifier());
HttpClient client=builder.setSSLSocketFactory(connectionFactory).build();
/*RestTemplate*/
RestTemplate restTemplate=new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
/*Execute*/
//HttpHeader
String base64Creds = "Basic "+new String(Base64.encodeBase64((username+":"+password).getBytes()));
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + base64Creds);
HttpEntity<Object> requestEntity=new HttpEntity<Object>(headers);
ResponseEntity<String> response=restTemplate.exchange(this.serverUrl+page,HttpMethod.GET, requestEntity, String.class,null);
方案二:在HttpClient
的CredentialsProvider
中维护UsernamePasswordCrendential
,以后可直接提交Request
/*HttpClient*/
HttpClientBuilder builder=HttpClientBuilder.create();
//1.SSL
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).useTLS().build();
SSLConnectionSocketFactory connectionFactory = new SSLConnectionSocketFactory(sslContext, new AllowAllHostnameVerifier());
builder.setSSLSocketFactory(connectionFactory);
//2.UsernamePasswordCredentials
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(authScope,new UsernamePasswordCredentials(username, password));
builder.setDefaultCredentialsProvider(credentialsProvider);
//3.build HttpClient
HttpClient client=builder.build();
/*RestTemplate*/
RestTemplate restTemplate=new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
/*Execute*/
ResponseEntity<String> response=restTemplate.getForEntity(this.serverUrl+page, String.class);
方案三:在HttpContext
的CredentialsProvider
中维护UsernamePasswordCrendential
,以后可直接提交Request
/*HttpClient*/
HttpClientBuilder builder=HttpClientBuilder.create();
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).useTLS().build();
SSLConnectionSocketFactory connectionFactory = new SSLConnectionSocketFactory(sslContext, new AllowAllHostnameVerifier());
HttpClient client=builder.setSSLSocketFactory(connectionFactory).build();
/*扩展HttpComponentsClientHttpRequestFactory的createHttpContext方法*/
//以后每次提交Request,都会调用BasicAuthRequestFactory的createHttpContext方法生成HttpContext附加到Request中
public class BasicAuthRequestFactory extends HttpComponentsClientHttpRequestFactory{
@Override
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri)
{
//1.set AuthScheme
HttpHost targetHost = new HttpHost(uri.getHost(), uri.getPort(),uri.getScheme());
AuthCache authCache = new BasicAuthCache();
BasicScheme basicAuth = new BasicScheme();
authCache.put(targetHost, basicAuth);
HttpClientContext localContext = HttpClientContext.create();
localContext.setAttribute(HttpClientContext.AUTH_CACHE, authCache);
//2.set CredentialsProvider
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(targetHost),new UsernamePasswordCredentials(username, password));
localContext.setCredentialsProvider(credentialsProvider);
return localContext;
}
}
/*RestTemplate*/
RestTemplate restTemplate=new RestTemplate(new BasicAuthRequestFactory(httpClient));
/*Execute*/
ResponseEntity<String> response=restTemplate.getForEntity(this.serverUrl+page, String.class);
构建DigestAuthentication
Authorization: Digest username="Mufasa",
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="0a4f113b",
response="6629fae49393a05397450978507c4ef1",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
PS:
其中nonce是digest认证的中心=base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime
是nonce的过期时间key
是放置nonce修改的私钥- 如果服务器生成的nonce已经过期(但是摘要还是有效)
DigestProcessingFilterEntryPoint
会发送一个"stale=true"
头信息- 这告诉用户代理,这里不再需要打扰用户(像是密码和用户其他都是正确的),只是简单尝试使用一个新nonce
- digest认证不使用session,所以无法与rememberMe同用
@Override
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri)
{
HttpHost targetHost = new HttpHost(uri.getHost(), uri.getPort(),uri.getScheme());
AuthCache authCache = new BasicAuthCache();
DigestScheme digestAuth = new DigestScheme();
digestAuth.overrideParamter("realm", realm);
digestAuth.overrideParamter("nonce", Long.toString(new Random().nextLong(), 36));
authCache.put(targetHost, digestAuth);
HttpClientContext localContext = HttpClientContext.create();
localContext.setAttribute(HttpClientContext.AUTH_CACHE, authCache);
if(this.username!=null){
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(targetHost),new UsernamePasswordCredentials(username, password));
localContext.setCredentialsProvider(credentialsProvider);
}
return localContext;
}
Server端进行认证
这里使用SpringSecurity
注意:
- 一定要设置
create-session="stateless"
- 表示Spring Security对登录成功的用户不会创建Session了,你的application也不会允许新建session,
- 且Spring Security会跳过所有的 filterChain:
HttpSessionSecurityContextRepository
,SessionManagementFilter
,RequestCacheFilter
.
- 多种authentication-manager,需设置id,而不是alias
- 此认证浏览器Browser会记住Authentication,故主要用于Restful API无状态请求的认证中
配置BasicAuthentication
<sec:http pattern="/vendor/**" realm="shuttle vendors" create-session="stateless" authentication-manager-ref="vendorAuthenticationManager">
<sec:intercept-url pattern="/vendor/**" access="ROLE_SUPPLIER" />
<sec:http-basic/>
</sec:http>
<sec:authentication-manager id="vendorAuthenticationManager">
<sec:authentication-provider>
<sec:user-service id="vendorUserService">
<sec:user name="S001" password="abc" authorities="ROLE_SUPPLIER" />
<sec:user name="S002" password="abc" authorities="ROLE_SUPPLIER" />
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
配置DigestAuthentication
<sec:http pattern="/vendor/**" create-session="stateless" entry-point-ref="digestEntryPoint" authentication-manager-ref="vendorAuthenticationManager">
<sec:intercept-url pattern="/vendor/**" access="ROLE_SUPPLIER" />
<sec:custom-filter ref="digestFilter" position="DIGEST_AUTH_FILTER" />
</sec:http>
<bean id="digestFilter" class="org.springframework.security.web.authentication.www.DigestAuthenticationFilter">
<property name="userDetailsService" ref="vendorUserService" />
<property name="authenticationEntryPoint" ref="digestEntryPoint" />
</bean>
<bean id="digestEntryPoint" class="org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint">
<property name="realmName" value="shuttle vendors" />
<property name="key" value="zero" />
</bean>
<sec:authentication-manager id="vendorAuthenticationManager">
<sec:authentication-provider>
<sec:user-service id="vendorUserService">
<sec:user name="S001" password="abc" authorities="ROLE_SUPPLIER" />
<sec:user name="S002" password="abc" authorities="ROLE_SUPPLIER" />
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>