目录
1. OAuth2.0授权服务
2. 资源服务
3. Gateway网关
4. 测试
在SpringSecurity+OAuth2.0 搭建认证中心和资源服务中心-CSDN博客
基础上整合网关和JWT实现分布式统一认证授权。
大致流程如下:
1、客户端发出请求给网关获取令牌
2、网关收到请求,直接转发给授权服务
3、授权服务验证用户名、密码等一系列身份,通过则颁发令牌给客户端
4、客户端携带令牌请求资源,请求直接到了网关层
5、网关对令牌进行校验(验签、过期时间校验....)、鉴权(对当前令牌携带的权限)和访问资源所需的权限进行比对,如果权限有交集则通过校验,直接转发给微服务
6、微服务进行逻辑处理
导入依赖
org.springframework.boot spring-boot-starter-weborg.springframework.cloud spring-cloud-starter-security2.2.5.RELEASE org.springframework.cloud spring-cloud-starter-oauth22.2.5.RELEASE mysql mysql-connector-java8.0.28 com.baomidou mybatis-plus-boot-starter3.3.2 org.springframework.security spring-security-oauth2-resource-serverorg.projectlombok lombok1.18.30 org.springframework.boot spring-boot-starter-testcom.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery
application.yaml
server: port: 8080 spring: application: name: oauth2-cloud-auth-server cloud: nacos: ## 注册中心配置 discovery: # nacos的服务地址,nacos-server中IP地址:端口号 server-addr: 127.0.0.1:8848 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/rbac?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: 123456
这里展示部分代码
AccessTokenConfig类
/** * 令牌的配置 */ @Configuration public class AccessTokenConfig { /** * JWT的秘钥 * TODO 实际项目中需要统一配置到配置文件中,资源服务也需要用到 */ private final static String SIGN_KEY="jwt"; /** * 令牌的存储策略 */ @Bean public TokenStore tokenStore() { //使用JwtTokenStore生成JWT令牌 return new JwtTokenStore(jwtAccessTokenConverter()); } /** * JwtAccessTokenConverter * TokenEnhancer的子类,在JWT编码的令牌值和OAuth身份验证信息之间进行转换。 * TODO:后期可以使用非对称加密 */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); // 设置秘钥 converter.setSigningKey(SIGN_KEY); return converter; } @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
MyAuthorizationConfig类
@Configuration @EnableAuthorizationServer public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter { @Autowired private TokenStore tokenStore; /** * 客户端存储策略,这里使用内存方式,后续可以存储在数据库 */ @Autowired private ClientDetailsService clientDetailsService; /** * Security的认证管理器,密码模式需要用到 */ @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; /** * 配置令牌访问的安全约束 */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security //开启/oauth/token_key验证端口权限访问 .tokenKeyAccess("permitAll()") //开启/oauth/check_token验证端口认证权限访问 .checkTokenAccess("permitAll()") //表示支持 client_id 和 client_secret 做登录认证 .allowFormAuthenticationForClients(); } //配置客户端 @Override public 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继续添加客户端 } @Bean public 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); //设置令牌增强,使用jwt services.setTokenEnhancer(jwtAccessTokenConverter); return services; } /** * 授权码模式的service,使用授权码模式authorization_code必须注入 */ @Bean public AuthorizationCodeServices authorizationCodeServices() { //授权码存在内存中 return new InMemoryAuthorizationCodeServices(); } /** * 配置令牌访问的端点 */ @Override public 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 { /** * 加密算法 */ @Autowired JwtTokenUserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { //todo 允许表单登录 http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl("/login") .permitAll() .and() .csrf() .disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //从数据库中查询用户信息 auth.userDetailsService(userDetailsService); } /** * AuthenticationManager对象在OAuth2认证服务中要使用,提前放入IOC容器中 * Oauth的密码模式需要 */ @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
导入依赖
org.springframework.boot spring-boot-starter-webcn.hutool hutool-allcom.alibaba fastjson1.2.78 org.projectlombok lombok1.18.30 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery
application.yaml
server: port: 8081 spring: application: name: oauth2-cloud-service cloud: nacos: ## 注册中心配置 discovery: # nacos的服务地址,nacos-server中IP地址:端口号 server-addr: 127.0.0.1:8848
AuthenticationFilter类
@Component public class AuthenticationFilter extends OncePerRequestFilter { /** * 具体方法主要分为两步 * 1. 解密网关传递的信息 * 2. 将解密之后的信息封装放入到request中 */ @Override protected 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]); //放入LoginVal LoginVal 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"); } }
导入依赖
org.springframework.cloud spring-cloud-starter-gatewayorg.springframework.cloud spring-cloud-starter-security2.2.5.RELEASE org.springframework.cloud spring-cloud-starter-oauth22.2.5.RELEASE org.springframework.security spring-security-oauth2-resource-servercom.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoveryorg.projectlombok lombokcn.hutool hutool-allcom.alibaba fastjson1.2.78 org.springframework.cloud spring-cloud-starter-loadbalancer
application.yaml
server: port: 7000 spring: main: allow-bean-definition-overriding: true application: name: oauth2-cloud-gateway cloud: nacos: ## 注册中心配置 discovery: # nacos的服务地址,nacos-server中IP地址:端口号 server-addr: 127.0.0.1:8848 gateway: ## 路由 routes: ## id只要唯一即可,名称任意 - id: oauth2-cloud-auth-server uri: lb://oauth2-cloud-auth-server predicates: ## Path Route Predicate Factory断言 - Path=/oauth/** - id: oauth2-cloud-order uri: lb://oauth2-cloud-service predicates: ## 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"; /** * 令牌的存储策略 */ @Bean public TokenStore tokenStore() { //使用JwtTokenStore生成JWT令牌 return new JwtTokenStore(jwtAccessTokenConverter()); } /** * JwtAccessTokenConverter * TokenEnhancer的子类,在JWT编码的令牌值和OAuth身份验证信息之间进行转换。 * TODO:后期可以使用非对称加密 */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); // 设置秘钥 converter.setSigningKey(SIGN_KEY); return converter; } }
JwtAccessManager类
@Slf4j @Component //经过认证管理器JwtAuthenticationManager认证成功后,就需要对令牌进行鉴权,如果该令牌无访问资源的权限,则不允通过。 public class JwtAccessManager implements ReactiveAuthorizationManager { @Override public Mono check(Mono mono, AuthorizationContext authorizationContext) { URI uri = authorizationContext.getExchange().getRequest().getURI(); //设计权限角色,这里简单写一下,实际上应该从数据库或者缓存中获取 Listauthorities = 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令牌进行解析令牌 */ @Autowired private TokenStore tokenStore; @Override public Mono 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从数据库获取不到OAuth2AccessToken if (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的鉴权管理器 */ @Autowired private ReactiveAuthorizationManager accessManager; @Autowired private RequestAuthenticationEntryPoint requestAuthenticationEntryPoint; @Autowired private RequestAccessDeniedHandler requestAccessDeniedHandler; /** * 系统参数配置 */ @Autowired private SysParameterConfig sysConfig; /** * token校验管理器 */ @Autowired private ReactiveAuthenticationManager tokenAuthenticationManager; @Autowired private CorsFilter corsFilter; @Bean SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception{ //认证过滤器,放入认证管理器tokenAuthenticationManager AuthenticationWebFilter 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 { @Override public Monohandle(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令牌的服务 */ @Autowired private TokenStore tokenStore; /** * 系统参数配置 */ @Autowired private SysParameterConfig sysConfig; @Override public Monofilter(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 判断是否是有效的token OAuth2AccessToken oAuth2AccessToken; try { //解析token,使用tokenStore oAuth2AccessToken = tokenStore.readAccessToken(token); Map additionalInformation = oAuth2AccessToken.getAdditionalInformation(); System.out.println(additionalInformation); //取出用户身份信息 String user_name = additionalInformation.get("user_name").toString(); //获取用户权限 List authorities = (List ) 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); } } @Override public int getOrder() { return 0; } /** * 对url进行校验匹配 */ private boolean checkUrls(List 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 invalidTokenMono(ServerWebExchange exchange) { return buildReturnMono(ResultMsg.builder() .code(1004) .msg("无效的token") .build(), exchange); } private Mono 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 ListignoreUrls; }
代码链接:Gateway+Springsecurity+OAuth2.0+JWT实现分布式统一认证授权资源-CSDN文库