赞
踩
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.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.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.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.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.1 通过网关去获取token
6.2 带着token 访问资源服务器 拿到当前登录人的信息
大功告成:项目源码 GitHub直达
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。