1.我们把三个服务分别注册到nacos中
1.1:pom文件中加入 nacos依赖(三个服务都得加)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
1.2:在application.yml中配置nacos连接地址
cloud:
nacos:
discovery:
#指定nacos server的地址
server-addr: 127.0.0.1:8848
2.配置gateway
2.1:在application.yml中配置 (注意配置顺序,顺序不对 gateway可能启动不起来)
server:
port: 9090
spring:
application:
name: springcloud-oauth-gateway
#将服务器注册到nacos中
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
##是否与服务发现组件进行结合,通过 serviceId 转发到具体的服务实例。默认为false,设为true便开启通过服务中心的自动根据 serviceId 创建路由的功能。
gateway:
discovery:
locator:
enabled: true
routes:
- id: springcloud-oauth-auth
uri: lb://springcloud-oauth-auth
predicates:
- Path=/auth/**
- id: springcloud-oauth-resource
uri: lb://springcloud-oauth-resource
predicates:
- Path=/resource/**
datasource:
driver-class-name: com.mysql.jdbc.Driver
password: zhangxiaoyun123
url: jdbc:mysql://192.168.3.37/zxy_system_service?useUnicode=true&characterEncoding=utf8&useSSL=false&zeroDateTimeBehavior=convertToNull
username: root
##这个的意思 是 在spring cloud服务太多的情况下 bean名字重名的情况下多 设置为true 后重名的bean覆盖以前的bean
main:
allow-bean-definition-overriding: true
ribbon:
MaxAutoRetries: 2 #最大重试次数,当nacos中可以找到服务,但是服务连不上时将会重试,如果nacos中找不到服务则直接走断路器
MaxAutoRetriesNextServer: 3 #切换实例的重试次数
OkToRetryOnAllOperations: false #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 6000 #请求处理的超时时间
##调用方法的超时时间 在调用方配置,被该调用方的所有方法的超时时间都是该值,优先级低于下边的指定配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeout-in-milliseconds: 6000
3.封装获取token的方法
3.1:创建TokenService
import com.example.springcloudalibabaoauthauth.util.AuthToken;
/**
* 2 * @Author: ZhangShuai
* 3 * @Date: 2020/6/18 14:44
* 4
*/
public interface TokenService {
/**
* 根据用户名密码获取令牌
*
* @param username 用户名
* @param password 用户密码
* @return
*/
AuthToken passwordVerifyToken(String username, String password);
}
3.2:实现TokenService
注(这里将获取到的token存储到redis中,当访问网关的时候,去判断hadler中有没有token,判断这个token是否使用,可用才放行token)
import com.example.springcloudalibabaoauthauth.config.RedisMethod;
import com.example.springcloudalibabaoauthauth.service.TokenService;
import com.example.springcloudalibabaoauthauth.util.AuthToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.net.URI;
import java.util.Map;
/**
* 2 * @Author: ZhangShuai
* 3 * @Date: 2020/6/18 14:47
* 4
*/
@Service
public class TokenServiceImpl implements TokenService {
/** 这里指的是客户端的信息和密钥 具体看数据库表 oauth_client_details **/
private final String clientId="client";
private final String clientSecret="admin";
//在redis里面的过期时间 3600秒
private final Long ExpireTime=3600l;
@Autowired
LoadBalancerClient loadBalancerClient;
@Resource(name = "facePlusRestTemplate")
RestTemplate restTemplate;
@Autowired
RedisMethod redisMethod;
@Override
public AuthToken passwordVerifyToken(String username, String password) {
//拿着username 和password 去请求 /auth/oauth/token 来获取到token
AuthToken token = this.passwordApplyToken(username, password);
//如果错误信息 不为空的话 走 redis
if (token.getError() == null && token.getError_description() == null) {
//把token存储到redis当中
redisMethod.setStringTime(token.getAccess_token(), token.toString(), ExpireTime);
}
return token;
}
private AuthToken passwordApplyToken(String username, String password) {
//从nacos中获取认证服务的实例地址(因为spring security在认证服务中) 服务的名称
ServiceInstance serviceInstance = loadBalancerClient.choose("springcloud-oauth-auth");
//此地址就是http://ip:portx
URI uri = serviceInstance.getUri();
//令牌申请的地址 http://localhost:8086/auth/oauth/token
String authUrl = uri + "/auth/oauth/token";
//定义header
LinkedMultiValueMap<String, String> header = new LinkedMultiValueMap<>();
String httpBasic = getHttpBasic();
header.add("Authorization", httpBasic);
//定义body
LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "password");
body.add("username", username);
body.add("password", password);
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(body, header);
//调用登录认证服务 生成jwt令牌
ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, httpEntity, Map.class);
//申请令牌信息
AuthToken authToken = makeAuthToken(exchange);
return authToken;
}
/**
* 设置token值
*
* @param exchange 远程调用结果
* @return
*/
private AuthToken makeAuthToken(ResponseEntity<Map> exchange) {
Map bodyMap = exchange.getBody();
AuthToken authToken = new AuthToken();
if (bodyMap == null ||
bodyMap.get("access_token") == null ||
bodyMap.get("refresh_token") == null ||
bodyMap.get("jti") == null ||
bodyMap.get("expires_in") == null
) {
authToken.setError((String) bodyMap.get("error"));
authToken.setError_description((String) bodyMap.get("error_description"));
return authToken;
}
authToken.setAccess_token((String) bodyMap.get("access_token"));//用户身份令牌
authToken.setRefresh_token((String) bodyMap.get("refresh_token"));//刷新令牌
authToken.setJwt_token((String) bodyMap.get("jti"));//jwt令牌
return authToken;
}
/*获取httpBasic的串*/
private String getHttpBasic() {
String string = clientId + ":" + clientSecret;
//将串进行base64编码
byte[] encode = Base64Utils.encode(string.getBytes());
return "Basic " + new String(encode);
}
}
3.3:具体返回实体类可查看代码
4.配置gateway网关拦截器
4.1 GateWayGloFilter:网关拦截器,所有的请求 都会请求网关,网关来进行转发请求
import com.alibaba.fastjson.JSON;
import com.example.springcloudalibabaoatuhgatewat.config.RedisMethod;
import com.example.springcloudalibabaoatuhgatewat.entity.UserJwtVo;
import com.example.springcloudalibabaoatuhgatewat.service.AuthService;
import com.example.springcloudalibabaoatuhgatewat.util.AuthToken;
import com.example.springcloudalibabaoatuhgatewat.util.JwtUtils;
import com.example.springcloudalibabaoatuhgatewat.util.ResponseCodeEnum;
import com.example.springcloudalibabaoatuhgatewat.util.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 2 * @Author: ZhangShuai
* 3 * @Date: 2020/6/18 10:11
* 4
*/
@Slf4j
@Component
public class GateWayGloFilter implements GlobalFilter , Ordered {
public static final String Authorization = "Authorization";
//在redis里面的过期时间 3600秒
private final Long ExpireTime=3600l;
/*不需要身份验证的路径 */
public static final String NO_AUTH_PATH = "/auth/password/login,/auth/oauth/check_token,/auth/oauth/token";
@Autowired
private AuthService authService;
@Autowired
RedisMethod redisMethod;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取response对象
ServerHttpResponse serverHttpResponse = exchange.getResponse();
//获取request对象
ServerHttpRequest request = exchange.getRequest();
//没有登录 判断是否是白名单请求
if (isAllowRequesr(request)) {
//如果是 放行
return chain.filter(exchange);
}
try {
//获取header中的token
List<String> tokenlist = request.getHeaders().get("token");
if (CollectionUtils.isEmpty(tokenlist)) {
//没有token,拦截请求
return getVoidMono(serverHttpResponse, ResponseCodeEnum.TOKEN_MISSION);
}
String token = tokenlist.get(0);
//如果当前token 不在 redis里面 那就证明当前 token是假的 token
//如果是假的token 那么 请你去 认证服务器 认证去
String Token = redisMethod.getString(token);
if (StringUtils.isEmpty(Token)) {
//把异常返回过去 告诉 他
return getVoidMono(serverHttpResponse, ResponseCodeEnum.TOKEN_INVALID);
}
//如果当你 redis里面的时间 低于了多少秒之后 我去刷新你的token
long time = redisMethod.getTime(token);
//当用户登录超过三小时就重新刷新令牌
if (time < 50) {
//刷新token
List<String> refreshTokenlist = request.getHeaders().get("refreshToken");
String refreshToken = refreshTokenlist.get(0);
//调用服务重新生成令牌
AuthToken authToken = authService.refresh_token(refreshToken);
if (!org.springframework.util.StringUtils.isEmpty(authToken)) {
String jsonString = JSON.toJSONString(authToken);
//删除Redis原有的令牌 并存入新的令牌
if (redisMethod.delString(token)) {
redisMethod.setStringTime(authToken.getAccess_token(), jsonString, ExpireTime);
//将令牌响应给前端
return returnsToken(serverHttpResponse, authToken);
}
}
}
//去认证服务器登录
//通过jwt对token进行解析获取用户信息
UserJwtVo userJwtFromHeader = JwtUtils.getUserJwtFromToken(token);
log.info("用户{}正在访问资源:{}", userJwtFromHeader.getName(), request.getPath());
//权限判断 校验通过,请求头增强,放行
// Map map = JwtUtils.parsingJwt(token);
// String authorities = map.get("authorities").toString();
// System.out.println(authorities);
// if (!authorities.contains(path)) {
// return getVoidMono(serverHttpResponse, ResponseCodeEnum.REFRESH_TOKEN_QUANXIANNOT);
// }
//增强请求头
request.mutate().header(Authorization, "Bearer " + token, "token", token);
// System.out.println(request.getHeaders().toString());
//授权下面的服务 并且放行
//放行
return chain.filter(exchange);
} catch (Exception e) {
log.info("服务解析用户信息失败:", e);
//内部异常 返回500
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return exchange.getResponse().setComplete();
}
}
/**
* 创建response返回提示信息
*
* @param serverHttpResponse
* @param responseCodeEnum
* @return
*/
private Mono<Void> getVoidMono(ServerHttpResponse serverHttpResponse, ResponseCodeEnum responseCodeEnum) {
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
ResponseResult responseResult = ResponseResult.error(responseCodeEnum.getCode(), responseCodeEnum.getMessage());
DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(JSON.toJSONString(responseResult).getBytes());
return serverHttpResponse.writeWith(Flux.just(dataBuffer));
}
/**
* 创建response返回刷新令牌
*
* @param serverHttpResponse
* @param o
* @return
*/
private Mono<Void> returnsToken(ServerHttpResponse serverHttpResponse, Object o) {
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(JSON.toJSONString(o).getBytes());
return serverHttpResponse.writeWith(Flux.just(dataBuffer));
}
/**
* 判断请求是否在白名单
*
* @param request
* @return
*/
private boolean isAllowRequesr(ServerHttpRequest request) {
//获取当前请求的path和method
String path = request.getPath().toString();
String method = request.getMethodValue();
//判断是否允许
if (StringUtils.startsWith(NO_AUTH_PATH, path)) {
//是许可的路径 放行
return true;
}
//不是白名单请求
return false;
}
/*设置当前类在spring中的加载顺序*/
@Override
public int getOrder() {
return -2;
}
}
4.2 AuthService:当redis中token快过期的时候,拿着access_token去刷新token的类
import com.example.springcloudalibabaoatuhgatewat.util.AuthToken;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.net.URI;
import java.util.Map;
@Slf4j
@Service
public class AuthService {
/** 这里指的是客户端的信息和密钥 具体看数据库表 oauth_client_details **/
private final String clientId="client";
private final String clientSecret="admin";
@Autowired
LoadBalancerClient loadBalancerClient;
@Resource(name = "facePlusRestTemplate")
RestTemplate restTemplate;
/**
* 调用服务 获取刷新令牌
*
* @param token
* @return
*/
public AuthToken refresh_token(String token) {
//从nacos中获取认证服务的实例地址(因为spring security在认证服务中) 服务的名称
ServiceInstance serviceInstance = loadBalancerClient.choose("springcloud-oauth-auth");
//此地址就是http://ip:portx
URI uri = serviceInstance.getUri();
//令牌校验的地址 http://localhost:8086/auth/oauth/check_token
String authUrl = uri + "/auth/oauth/token";
//定义header
LinkedMultiValueMap<String, String> header = new LinkedMultiValueMap<>();
String httpBasic = getHttpBasic(clientId, clientSecret);
header.add("Authorization", httpBasic);
//定义body
LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "refresh_token");
body.add("refresh_token", token);
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(body, header);
//调用登录认证服务 刷新jwt令牌
ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, httpEntity, Map.class);
//申请令牌信息
AuthToken authToken = makeAuthToken(exchange);
return authToken;
}
private AuthToken makeAuthToken(ResponseEntity<Map> exchange) {
Map bodyMap = exchange.getBody();
AuthToken authToken = new AuthToken();
if (bodyMap == null ||
bodyMap.get("access_token") == null ||
bodyMap.get("refresh_token") == null ||
bodyMap.get("jti") == null ||
bodyMap.get("expires_in") == null
) {
authToken.setError((String) bodyMap.get("error"));
authToken.setError_description((String) bodyMap.get("error_description"));
return authToken;
}
authToken.setAccess_token((String) bodyMap.get("access_token"));//用户身份令牌
authToken.setRefresh_token((String) bodyMap.get("refresh_token"));//刷新令牌
authToken.setJwt_token((String) bodyMap.get("jti"));//jwt令牌
return authToken;
}
//获取httpbasic的串
private String getHttpBasic(String clientId, String clientSecret) {
String string = clientId + ":" + clientSecret;
//将串进行base64编码
byte[] encode = Base64Utils.encode(string.getBytes());
return "Basic " + new String(encode);
}
}
5.资源服务器拿到用户的登录信息
5.1 在 第二章节的时候 我们配置了 CustomUserInformationConverter 自定义jwt信息这个类 ,所以我们在资源服务器可以拿到这个类定义的东西 ,配置资源服务器 jwt解析的类
5.2 CustomUserInformationConverter :解析jwt内容类
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 2 * @Author: ZhangShuai
* 3 * @Date: 2020/6/12 17:03
* 4 资源服务器获取 认证服务器的自定义内容
*/
@Component
public class CustomUserInformationConverter extends DefaultAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
OAuth2Authentication authentication
= super.extractAuthentication(claims);
authentication.setDetails(claims);
return authentication;
}
}
5.3 JwtTokenStoreConfig:设置jwt的解析类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import javax.annotation.Resource;
/**
* 2 * @Author: ZhangShuai
* 3 * @Date: 2020/6/12 17:03
* 4 token使用jwt的配置
*/
@Configuration
public class JwtTokenStoreConfig {
public static final String EncryptedFile = "xmzs-jwt.jks";
public static final String Secret = "557554";
public static final String EncryptedFileName = "557554";
@Resource
private CustomUserInformationConverter customUserInformation;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter(customUserInformation));
}
//使用非对称加密算法
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserInformationConverter customUserInformationApadter) {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(EncryptedFileName);
converter.setAccessTokenConverter(customUserInformationApadter);
// //找到 加密文件所在的地址 读取classpath里面的 xm-jwt.jks文件
// ClassPathResource pathResource = new ClassPathResource(EncryptedFile);
// //秘钥配置时的密码
// KeyStoreKeyFactory keyFactory = new KeyStoreKeyFactory(pathResource, Secret.toCharArray());
// //使用keystore得到秘钥
// KeyPair pair = keyFactory.getKeyPair(EncryptedFileName);
// //设置秘钥
// converter.setKeyPair(pair);
//DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
//accessTokenConverter.setUserTokenConverter(customUserInformationApadter);
return converter;
}
}
5.4 ResourceServerConfig :在资源服务器配置该token解析类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
/**
* 2 * @Author: ZhangShuai
* 3 * @Date: 2020/6/18 11:52
* 4 这个注解表示 当前是一个资源服务器
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
TokenStore tokenStore;
/**
* 自定义 资源服务器 权限认证失败的异常
*/
@Bean
public AccessDeniedHandler getAccessDeniedHandler() {
return new ResourceAccessDeniedHandler();
}
/**
* 自定义 资源服务器 未经授权异常
*
* @return
*/
@Bean
public AuthenticationEntryPoint getAuthenticationEntryPoint() {
return new ResourceAuthenticationEntryPoint();
}
/**
* 设置资源服务器的 token 和自定义的异常
*
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore)
.accessDeniedHandler(getAccessDeniedHandler())
.authenticationEntryPoint(getAuthenticationEntryPoint());
}
}
5.5 Subject :配置当前登录人的信息 (这里面是自定义的)
import lombok.Data;
/**
* 2 * @Author: ZhangShuai
* 3 * @Date: 2020/6/19 14:28
* 4
*/
@Data
public class Subject {
/**
* 用户id
*/
private Long id;
/**
* 用户名
*/
private String username;
}
5.6 GetSubject:获取jwt里面自定义的内容 并且赋值给 Subject类 (这里面是自定义的)
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
/**
* 2 * @Author: ZhangShuai
* 3 * @Date: 2020/6/19 14:30
* 4 获取当前用户的信息
*/
@Component
public class GetSubject {
public Subject getSubject(OAuth2Authentication oAuth2Authentication) {
OAuth2AuthenticationDetails oAuth2AuthenticationDetails = (OAuth2AuthenticationDetails) oAuth2Authentication.getDetails();
LinkedHashMap details = (LinkedHashMap) oAuth2AuthenticationDetails.getDecodedDetails();
Subject subject=new Subject();
//获取自定义的id
subject.setId(Long.parseLong(details.get("id").toString()));
//
subject.setUsername(String.valueOf(details.get("username")));
return subject;
}
}
5.7 获取当前用户的信息
import com.example.springcloudalibabaoauthresource.util.GetSubject;
import com.example.springcloudalibabaoauthresource.util.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 2 * @Author: ZhangShuai
* 3 * @Date: 2020/6/15 8:47
* 4
*/
@RestController
public class HelloController {
@Autowired
GetSubject getSubject;
@GetMapping("/hello")
public String hello(OAuth2Authentication oAuth2Authentication) {
Subject subject = getSubject.getSubject(oAuth2Authentication);
System.out.println("subject = " + subject);
return "Hello";
}
}
6.认证和授权的演示
6.1 通过网关去获取token
6.2 带着token 访问资源服务器 拿到当前登录人的信息
注(请修改 jdbc的连接 和 redis的连接 )