单点登陆
一处登录,处处登录,在微服务开发之中会存在很多的服务,当用户在某个服务之中登录成功,那么在这整个项目之中都登录成功
解决方案:Apache Shiro. CAS Spring security
Oauth2
Oauth2是一种开放资源授权的标准,是一种授权机制,主要用来颁发令牌(token)。主要常用的有两种授权模式:授权码模式和密码模式,此外还有隐藏式和客户端凭证两种方式
注意:不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
密码模式
我在此详细讲讲密码模式的用户认证,授权码模式可参考其他。
1.当用户访问我们的资源服务时,若是已登录,cookie中将会存入短令牌jti,没有认证登录,cookie之中不存在短令牌
2.请求进入网关之后,如果没有短令牌jti存在并且不是登录请求的话,那么网关将直接返回请求至登录页面进行登录认证,用户使用账户密码的模式登录,登录请求将在网关中被直接放行进入认证服务进行认证,
3.认证服务会访问oauth2的封装接口/oauth/token来验证用户密码是否正确,如果验证通过秘钥证书签发token令牌相关信息同时进行授权,并且将其中的jti:token存入redis当中,jti为key,token为值,原因是token会很长,cookie可能放不下,cookie中将存入jti短令牌,用于其他服务来鉴权
4.当用户登录认证成功,cookie中已存入相应的jti短令牌时,再次访问其他被保护的资源,将会被网关进行增强,将redis中jwt令牌取出放入request请求头文件中放行,
5.在资源服务中放有公钥,配合security框架,资源服务将对携带在请求头文件的令牌进行解析,获取其中的用户信息,尤其是授权信息,在资源服务中的每个接口都会有相应的权限访问保护,由下面注解提供权限校验,如果具有相应权限,资源服务对请求放行,返回相应的数据给用户。
@PreAuthorize("hasAnyAuthority('seckill_list')") //只有拥有seckill_list权限的人才可访问本接口
access_token:返回的令牌token
refresh_token:刷新令牌,用于令牌过期时使用,使用该令牌可以让令牌过期时间刷新
expires_in:过期时间
Jti:短令牌,将token存入redis,jti为key,并将jti存入cookie用于绕过认证服务访问被保护资源
微服务之间的认证
网关与资源服务直接的访问将通过request请求头中的jwt令牌信息完成鉴权认证,但是如果由网关进入一个资源服务,该服务需要去调用另外一个微服务来获取数据,那么微服务间的认证该如何进行?将如何把令牌从一个微服务传给另外一个微服务?
由于微服务之间不像网关与微服务直接通过request的头文件传递令牌,从而让微服务拥有认证权限,微服务之间通过feign进行远程调用,没有传递request头文件,使得令牌无法传递,此时的解决方法是使用feign的拦截器,每次微服务调用之前都先检查下头文件,将请求的头文件中的令牌数据再放入到header中,再调用其他微服务即可。
微服务之间的认证很频繁,将拦截器放在common公共模块,拦截器将请求头文件中的jwt信息拦截并且存入header中,当某个服务需要feign远程调用其他服务时,在启动类注入启用该拦截器,即可进行令牌的传递完成认证
启动类启用拦截器:
@Bean
public FeignInterceptor feignInterceptor(){
return new FeignInterceptor();
}
拦截器实现代码:
package com.changgou.interceptor;
import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
@Component
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
//获得请求属性对象
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//获得request对象
if (requestAttributes!=null){
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
if (request!=null){
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames!=null){
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
if ("authorization".equals(headerName)){
String headValue=request.getHeader(headerName);
requestTemplate.header(headerName,headValue);
}
}
}
}
}
}
}
认证服务主要代码
package com.changgou.oauth.service.impl;
import com.changgou.oauth.service.AuthService;
import com.changgou.oauth.util.AuthToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${auth.ttl}")
private long ttl;
/**
* 认证登录功能
* @param username 用户名
* @param password 密码
* @param clientId 客户端Id
* @param clientSecret 客户端秘钥
* @return
*/
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret) {
//1.申请令牌
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
URI uri = serviceInstance.getUri();
String url=uri+"/oauth/token";
MultiValueMap<String, String> body=new LinkedMultiValueMap<>();
//选择模式,密码模式
body.add("grant_type","password");
body.add("username",username);
body.add("password",password);
MultiValueMap<String, String> headers=new LinkedMultiValueMap<>();
//进行http basic认证
headers.add("Authorization",this.getBasic(clientId,clientSecret));
HttpEntity<MultiValueMap<String,String>> requestEntity=new HttpEntity<>(body,headers);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode()!=401&&response.getRawStatusCode()!=400) {
super.handleError(response);
}
}
});
//核心代码,访问oauth2接口来获取token信息
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
Map entityBody = responseEntity.getBody();
if (CollectionUtils.isEmpty(entityBody)||entityBody.get("access_token")==null||entityBody.get("refresh_token")==null||entityBody.get("jti")==null){
//申请令牌失败
throw new RuntimeException("申请令牌失败");
}
//2.封装数据
AuthToken authToken = new AuthToken();
authToken.setAccessToken((String) entityBody.get("access_token"));
authToken.setRefreshToken((String) entityBody.get("refresh_token"));
authToken.setJti((String) entityBody.get("jti"));
//3.将jti:token存入redis
stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl,TimeUnit.SECONDS);
return authToken;
}
private String getBasic(String clientId, String clientSecret) {
String value=clientId+":"+clientSecret;
byte[] encode = Base64Utils.encode(value.getBytes());
return "Basic "+new String(encode);
}
}
授权码模式
获取用户信息之后,再根据信息关联查询用户表数据库,如果是第一次扫码登录,需要增加一条数据,根据我们的用户数据生成一个令牌给用户便于他下次登录
授权码模式详解:
https://www.jianshu.com/p/f3da08865ffb