目录
1. OAuth2.0授权服务
2. 资源服务
3. Gateway网关
4. 测试
在SpringSecurity+OAuth2.0 搭建认证中心和资源服务中心-CSDN博客
基础上整合网关和JWT实现分布式统一认证授权。
大致流程如下:
1、客户端发出请求给网关获取令牌
2、网关收到请求,直接转发给授权服务
3、授权服务验证用户名、密码等一系列身份,通过则颁发令牌给客户端
4、客户端携带令牌请求资源,请求直接到了网关层
5、网关对令牌进行校验(验签、过期时间校验....)、鉴权(对当前令牌携带的权限)和访问资源所需的权限进行比对,如果权限有交集则通过校验,直接转发给微服务
6、微服务进行逻辑处理
1. OAuth2.0授权服务
导入依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId><version>2.2.5.RELEASE</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.5.RELEASE</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.28</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-resource-server</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency></dependencies>
application.yaml
server:port: 8080
spring:application:name: oauth2-cloud-auth-servercloud:nacos:## 注册中心配置discovery:# nacos的服务地址,nacos-server中IP地址:端口号server-addr: 127.0.0.1:8848datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/rbac?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCusername: rootpassword: 123456
这里展示部分代码
AccessTokenConfig类
/*** 令牌的配置*/
@Configuration
public class AccessTokenConfig {/*** JWT的秘钥* TODO 实际项目中需要统一配置到配置文件中,资源服务也需要用到*/private final static String SIGN_KEY="jwt";/*** 令牌的存储策略*/@Beanpublic TokenStore tokenStore() {//使用JwtTokenStore生成JWT令牌return new JwtTokenStore(jwtAccessTokenConverter());}/*** JwtAccessTokenConverter* TokenEnhancer的子类,在JWT编码的令牌值和OAuth身份验证信息之间进行转换。* TODO:后期可以使用非对称加密*/@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter(){JwtAccessTokenConverter converter = new JwtAccessTokenConverter();// 设置秘钥converter.setSigningKey(SIGN_KEY);return converter;}@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}
MyAuthorizationConfig类
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate TokenStore tokenStore;/*** 客户端存储策略,这里使用内存方式,后续可以存储在数据库*/@Autowiredprivate ClientDetailsService clientDetailsService;/*** Security的认证管理器,密码模式需要用到*/@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate JwtAccessTokenConverter jwtAccessTokenConverter;/*** 配置令牌访问的安全约束*/@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security//开启/oauth/token_key验证端口权限访问.tokenKeyAccess("permitAll()")//开启/oauth/check_token验证端口认证权限访问.checkTokenAccess("permitAll()")//表示支持 client_id 和 client_secret 做登录认证.allowFormAuthenticationForClients();}//配置客户端@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {//内存模式clients.inMemory()//客户端id.withClient("test")//客户端秘钥.secret(new BCryptPasswordEncoder().encode("123456"))//资源id,唯一,比如订单服务作为一个资源,可以设置多个.resourceIds("order")//授权模式,总共四种,1. authorization_code(授权码模式)、password(密码模式)、client_credentials(客户端模式)、implicit(简化模式)//refresh_token并不是授权模式,.authorizedGrantTypes("authorization_code","password","client_credentials","implicit","refresh_token")//允许的授权范围,客户端的权限,这里的all只是一种标识,可以自定义,为了后续的资源服务进行权限控制.scopes("all")//false 则跳转到授权页面.autoApprove(false)//授权码模式的回调地址.redirectUris("http://www.baidu.com"); //可以and继续添加客户端}@Beanpublic AuthorizationServerTokenServices tokenServices() {DefaultTokenServices services = new DefaultTokenServices();//客户端端配置策略services.setClientDetailsService(clientDetailsService);//支持令牌的刷新services.setSupportRefreshToken(true);//令牌服务services.setTokenStore(tokenStore);//access_token的过期时间services.setAccessTokenValiditySeconds(60 * 60 * 2);//refresh_token的过期时间services.setRefreshTokenValiditySeconds(60 * 60 * 24 * 3);//设置令牌增强,使用jwtservices.setTokenEnhancer(jwtAccessTokenConverter);return services;}/*** 授权码模式的service,使用授权码模式authorization_code必须注入*/@Beanpublic AuthorizationCodeServices authorizationCodeServices() {//授权码存在内存中return new InMemoryAuthorizationCodeServices();}/*** 配置令牌访问的端点*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints//授权码模式所需要的authorizationCodeServices.authorizationCodeServices(authorizationCodeServices())//密码模式所需要的authenticationManager.authenticationManager(authenticationManager)//令牌管理服务,无论哪种模式都需要.tokenServices(tokenServices())//只允许POST提交访问令牌,uri:/oauth/token.allowedTokenEndpointRequestMethods(HttpMethod.POST);}
}
SecurityConfig类
/*** spring security的安全配置*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 加密算法*/@AutowiredJwtTokenUserDetailsService userDetailsService;@Overrideprotected void configure(HttpSecurity http) throws Exception {//todo 允许表单登录http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginProcessingUrl("/login").permitAll().and().csrf().disable();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//从数据库中查询用户信息auth.userDetailsService(userDetailsService);}/*** AuthenticationManager对象在OAuth2认证服务中要使用,提前放入IOC容器中* Oauth的密码模式需要*/@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}
2. 资源服务
导入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.78</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
application.yaml
server:port: 8081
spring:application:name: oauth2-cloud-servicecloud:nacos:## 注册中心配置discovery:# nacos的服务地址,nacos-server中IP地址:端口号server-addr: 127.0.0.1:8848
AuthenticationFilter类
@Component
public class AuthenticationFilter extends OncePerRequestFilter {/*** 具体方法主要分为两步* 1. 解密网关传递的信息* 2. 将解密之后的信息封装放入到request中*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取请求头中的用户信息String token = request.getHeader("token");if (token!=null){//解密String json = Base64.decodeStr(token);JSONObject jsonObject = JSON.parseObject(json);//获取用户身份信息、权限信息String principal = jsonObject.getString("user_name");JSONArray tempJsonArray = jsonObject.getJSONArray("authorities");//权限String[] authorities = tempJsonArray.toArray(new String[0]);//放入LoginValLoginVal loginVal = new LoginVal();loginVal.setUsername(principal);loginVal.setAuthorities(authorities);//放入request的attribute中request.setAttribute("login_message",loginVal);}filterChain.doFilter(request,response);}
}
ServiceController类
@RestController
public class ServiceController {@RequestMapping("/test")public LoginVal test(HttpServletRequest httpServletRequest){return (LoginVal)httpServletRequest.getAttribute("login_message");}
}
3. Gateway网关
导入依赖
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId><version>2.2.5.RELEASE</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.5.RELEASE</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-resource-server</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.78</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency></dependencies>
application.yaml
server:port: 7000
spring:main:allow-bean-definition-overriding: trueapplication:name: oauth2-cloud-gatewaycloud:nacos:## 注册中心配置discovery:# nacos的服务地址,nacos-server中IP地址:端口号server-addr: 127.0.0.1:8848gateway:## 路由routes:## id只要唯一即可,名称任意- id: oauth2-cloud-auth-serveruri: lb://oauth2-cloud-auth-serverpredicates:## Path Route Predicate Factory断言- Path=/oauth/**- id: oauth2-cloud-orderuri: lb://oauth2-cloud-servicepredicates:## Path Route Predicate Factory断言- Path=/test/**oauth2:cloud:sys:parameter:ignoreUrls:- /oauth/token- /oauth/authorize
AccessTokenConfig类
/*** 令牌的配置*/
@Configuration
public class AccessTokenConfig {private final static String SIGN_KEY="jwt";/*** 令牌的存储策略*/@Beanpublic TokenStore tokenStore() {//使用JwtTokenStore生成JWT令牌return new JwtTokenStore(jwtAccessTokenConverter());}/*** JwtAccessTokenConverter* TokenEnhancer的子类,在JWT编码的令牌值和OAuth身份验证信息之间进行转换。* TODO:后期可以使用非对称加密*/@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter(){JwtAccessTokenConverter converter = new JwtAccessTokenConverter();// 设置秘钥converter.setSigningKey(SIGN_KEY);return converter;}
}
JwtAccessManager类
@Slf4j
@Component
//经过认证管理器JwtAuthenticationManager认证成功后,就需要对令牌进行鉴权,如果该令牌无访问资源的权限,则不允通过。
public class JwtAccessManager implements ReactiveAuthorizationManager<AuthorizationContext> {@Overridepublic Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {URI uri = authorizationContext.getExchange().getRequest().getURI();//设计权限角色,这里简单写一下,实际上应该从数据库或者缓存中获取List<String> authorities = new ArrayList<>();authorities.add("ROLE_admin");//认证通过且角色匹配的用户可访问当前路径return mono//判断是否认证成功.filter(Authentication::isAuthenticated)//获取认证后的全部权限.flatMapIterable(Authentication::getAuthorities).map(GrantedAuthority::getAuthority)//如果权限包含则判断为true.any(authorities::contains).map(AuthorizationDecision::new).defaultIfEmpty(new AuthorizationDecision(false));}}
JwtAuthenticationManager类
/*** JWT认证管理器,主要的作用就是对携带过来的token进行校验,比如过期时间,加密方式等* 一旦token校验通过,则交给鉴权管理器进行鉴权*/
@Component
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {/*** 使用JWT令牌进行解析令牌*/@Autowiredprivate TokenStore tokenStore;@Overridepublic Mono<Authentication> authenticate(Authentication authentication) {return Mono.justOrEmpty(authentication).filter(a -> a instanceof BearerTokenAuthenticationToken).cast(BearerTokenAuthenticationToken.class).map(BearerTokenAuthenticationToken::getToken).flatMap((accessToken -> {OAuth2AccessToken oAuth2AccessToken = this.tokenStore.readAccessToken(accessToken);//根据access_token从数据库获取不到OAuth2AccessTokenif (oAuth2AccessToken == null) {return Mono.error(new InvalidTokenException("无效的token!"));} else if (oAuth2AccessToken.isExpired()) {return Mono.error(new InvalidTokenException("token已过期!"));}OAuth2Authentication oAuth2Authentication = this.tokenStore.readAuthentication(accessToken);if (oAuth2Authentication == null) {return Mono.error(new InvalidTokenException("无效的token!"));} else {return Mono.just(oAuth2Authentication);}})).cast(Authentication.class);}
}
SecurityConfig类
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {/*** JWT的鉴权管理器*/@Autowiredprivate ReactiveAuthorizationManager<AuthorizationContext> accessManager;@Autowiredprivate RequestAuthenticationEntryPoint requestAuthenticationEntryPoint;@Autowiredprivate RequestAccessDeniedHandler requestAccessDeniedHandler;/*** 系统参数配置*/@Autowiredprivate SysParameterConfig sysConfig;/*** token校验管理器*/@Autowiredprivate ReactiveAuthenticationManager tokenAuthenticationManager;@Autowiredprivate CorsFilter corsFilter;@BeanSecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception{//认证过滤器,放入认证管理器tokenAuthenticationManagerAuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(tokenAuthenticationManager);authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());http.httpBasic().disable().csrf().disable().authorizeExchange()//白名单直接放行.pathMatchers(ArrayUtil.toArray(sysConfig.getIgnoreUrls(),String.class)).permitAll()//其他的请求必须鉴权,使用鉴权管理器.anyExchange().access(accessManager)//异常处理.and().exceptionHandling().authenticationEntryPoint(requestAuthenticationEntryPoint).accessDeniedHandler(requestAccessDeniedHandler).and()// 跨域过滤器.addFilterAt(corsFilter, SecurityWebFiltersOrder.CORS)//token的认证过滤器,用于校验token和认证.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);return http.build();}
}
RequestAccessDeniedHandler
/*** 自定义返回结果:没有权限访问时*/
@Component
public class RequestAccessDeniedHandler implements ServerAccessDeniedHandler {@Overridepublic Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.OK);response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);String body= JSONUtil.toJsonStr(new ResultMsg(1005,"无权限访问",null));DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));return response.writeWith(Mono.just(buffer));}
}
GlobalAuthenticationFilter
/*** 全局过滤器,对token的拦截,解析token放入header中,便于下游微服务获取用户信息* 分为如下几步:* 1、白名单直接放行* 2、校验token* 3、读取token中存放的用户信息* 4、重新封装用户信息,加密成功json数据放入请求头中传递给下游微服务*/
@Component
@Slf4j
public class GlobalAuthenticationFilter implements GlobalFilter, Ordered {/*** JWT令牌的服务*/@Autowiredprivate TokenStore tokenStore;/*** 系统参数配置*/@Autowiredprivate SysParameterConfig sysConfig;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String requestUrl = exchange.getRequest().getPath().value();//1、白名单放行,比如授权服务、静态资源.....if (checkUrls(sysConfig.getIgnoreUrls(),requestUrl)){return chain.filter(exchange);}//2、 检查token是否存在String token = getToken(exchange);if (StringUtils.isBlank(token)) {return invalidTokenMono(exchange);}//3 判断是否是有效的tokenOAuth2AccessToken oAuth2AccessToken;try {//解析token,使用tokenStoreoAuth2AccessToken = tokenStore.readAccessToken(token);Map<String, Object> additionalInformation = oAuth2AccessToken.getAdditionalInformation();System.out.println(additionalInformation);//取出用户身份信息String user_name = additionalInformation.get("user_name").toString();//获取用户权限List<String> authorities = (List<String>) additionalInformation.get("authorities");//将用户名和权限进行Base64加密JSONObject jsonObject=new JSONObject();jsonObject.put("user_name", user_name);jsonObject.put("authorities",authorities);String base = Base64.encode(jsonObject.toJSONString());// ServerHttpRequest 中的 mutate 方法是用于创建一个修改后的请求对象的方法,而不改变原始请求对象。这个方法是为了在处理请求过程中创建一个新的请求对象,以便进行一些修改或增强。ServerHttpRequest tokenRequest = exchange.getRequest().mutate().header("token",base).build();ServerWebExchange build = exchange.mutate().request(tokenRequest).build();return chain.filter(build);} catch (InvalidTokenException e) {//解析token异常,直接返回token无效return invalidTokenMono(exchange);}}@Overridepublic int getOrder() {return 0;}/*** 对url进行校验匹配*/private boolean checkUrls(List<String> urls,String path){AntPathMatcher pathMatcher = new AntPathMatcher();for (String url : urls) {if (pathMatcher.match(url,path))return true;}return false;}/*** 从请求头中获取Token*/private String getToken(ServerWebExchange exchange) {String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");if (StringUtils.isBlank(tokenStr)) {return null;}String token = tokenStr.split(" ")[1];if (StringUtils.isBlank(token)) {return null;}return token;}/*** 无效的token*/private Mono<Void> invalidTokenMono(ServerWebExchange exchange) {return buildReturnMono(ResultMsg.builder().code(1004).msg("无效的token").build(), exchange);}private Mono<Void> buildReturnMono(ResultMsg resultMsg, ServerWebExchange exchange) {ServerHttpResponse response = exchange.getResponse();byte[] bits = JSON.toJSONString(resultMsg).getBytes(StandardCharsets.UTF_8);DataBuffer buffer = response.bufferFactory().wrap(bits);response.setStatusCode(HttpStatus.UNAUTHORIZED);response.getHeaders().add("Content-Type", "application/json;charset:utf-8");return response.writeWith(Mono.just(buffer));}
}
SysParameterConfig
@ConfigurationProperties(prefix = "oauth2.cloud.sys.parameter")
@Data
@Component
public class SysParameterConfig {/*** 白名单*/private List<String> ignoreUrls;
}
4. 测试
代码链接:Gateway+Springsecurity+OAuth2.0+JWT实现分布式统一认证授权资源-CSDN文库