├── .gitignore ├── README.md ├── oauth2-auth ├── pom.xml └── src │ └── main │ ├── java │ └── cn │ │ └── gathub │ │ └── auth │ │ ├── Oauth2AuthApplication.java │ │ ├── api │ │ ├── CommonResult.java │ │ ├── IErrorCode.java │ │ └── ResultCode.java │ │ ├── component │ │ └── JwtTokenEnhancer.java │ │ ├── config │ │ ├── Oauth2ServerConfig.java │ │ ├── RedisRepositoryConfig.java │ │ └── WebSecurityConfig.java │ │ ├── constant │ │ ├── MessageConstant.java │ │ └── RedisConstant.java │ │ ├── controller │ │ ├── AuthController.java │ │ └── KeyPairController.java │ │ ├── domain │ │ ├── dto │ │ │ └── Oauth2TokenDto.java │ │ └── entity │ │ │ ├── Client.java │ │ │ └── User.java │ │ ├── exception │ │ └── Oauth2ExceptionHandler.java │ │ └── service │ │ ├── ClientService.java │ │ ├── ResourceService.java │ │ ├── UserService.java │ │ ├── impl │ │ ├── ClientServiceImpl.java │ │ ├── ResourceServiceImpl.java │ │ └── UserServiceImpl.java │ │ └── principal │ │ ├── ClientPrincipal.java │ │ └── UserPrincipal.java │ └── resources │ ├── application.yml │ └── jwt.jks ├── oauth2-gateway ├── pom.xml └── src │ └── main │ ├── java │ └── cn │ │ └── gathub │ │ └── gateway │ │ ├── Oauth2GatewayApplication.java │ │ ├── api │ │ ├── CommonResult.java │ │ ├── IErrorCode.java │ │ └── ResultCode.java │ │ ├── authorization │ │ └── AuthorizationManager.java │ │ ├── component │ │ ├── RestAuthenticationEntryPoint.java │ │ └── RestfulAccessDeniedHandler.java │ │ ├── config │ │ ├── IgnoreUrlsConfig.java │ │ ├── RedisRepositoryConfig.java │ │ └── ResourceServerConfig.java │ │ ├── constant │ │ ├── AuthConstant.java │ │ └── RedisConstant.java │ │ └── filter │ │ ├── AuthGlobalFilter.java │ │ └── IgnoreUrlsRemoveJwtFilter.java │ └── resources │ └── application.yml ├── oauth2-resource ├── pom.xml └── src │ └── main │ ├── java │ └── cn │ │ └── gathub │ │ └── resource │ │ ├── Oauth2ResourceApplication.java │ │ ├── controller │ │ ├── HelloController.java │ │ └── UserController.java │ │ └── domain │ │ └── User.java │ └── resources │ └── application.yml └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-cloud-gateway-oauth2 2 | 3 | ## 前言 4 | 我们理想的微服务权限解决方案应该是这样的,认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。 5 | ## 架构 6 | 通过认证服务(`oauth2-auth`)进行统一认证,然后通过网关(`oauth2-gateway`)来统一校验认证和鉴权。采用Nacos作为注册中心,Gateway作为网关,使用nimbus-jose-jwtJWT库操作JWT令牌。 7 | - oauth2-auth:Oauth2认证服务,负责对登录用户进行认证,整合Spring Security Oauth2 8 | - ouath2-gateway:网关服务,负责请求转发和鉴权功能,整合Spring Security Oauth2 9 | - oauth2-resource:受保护的API服务,用户鉴权通过后可以访问该服务,不整合Spring Security Oauth2 10 | ## 具体实现 11 | ### 一、认证服务`oauth2-auth` 12 | 13 | > 1、首先来搭建认证服务,它将作为Oauth2的认证服务使用,并且网关服务的鉴权功能也需要依赖它,在pom.xml中添加相关依赖,主要是Spring Security、Oauth2、JWT、Redis相关依赖 14 | 15 | ```java 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-web 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-security 24 | 25 | 26 | org.springframework.cloud 27 | spring-cloud-starter-oauth2 28 | 29 | 30 | com.nimbusds 31 | nimbus-jose-jwt 32 | 8.16 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-data-redis 38 | 39 | 40 | 41 | ``` 42 | 43 | > 2、在application.yml中添加相关配置,主要是Nacos和Redis相关配置 44 | 45 | ```yml 46 | server: 47 | port: 9401 48 | spring: 49 | profiles: 50 | active: dev 51 | application: 52 | name: oauth2-auth 53 | cloud: 54 | nacos: 55 | discovery: 56 | server-addr: localhost:8848 57 | jackson: 58 | date-format: yyyy-MM-dd HH:mm:ss 59 | redis: 60 | database: 0 61 | port: 6379 62 | host: localhost 63 | password: 64 | management: 65 | endpoints: 66 | web: 67 | exposure: 68 | include: "*" 69 | 70 | ``` 71 | 72 | > 3、使用keytool生成RSA证书jwt.jks,复制到resource目录下,在JDK的bin目录下使用如下命令即可 73 | 74 | ```shell 75 | keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks 76 | ``` 77 | 78 | > 4、创建UserServiceImpl类实现Spring Security的UserDetailsService接口,用于加载用户信息 79 | 80 | ```Java 81 | package cn.gathub.auth.service.impl; 82 | 83 | import org.springframework.security.authentication.AccountExpiredException; 84 | import org.springframework.security.authentication.CredentialsExpiredException; 85 | import org.springframework.security.authentication.DisabledException; 86 | import org.springframework.security.authentication.LockedException; 87 | import org.springframework.security.core.userdetails.UserDetails; 88 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 89 | import org.springframework.security.crypto.password.PasswordEncoder; 90 | import org.springframework.stereotype.Service; 91 | 92 | import java.util.ArrayList; 93 | import java.util.List; 94 | import java.util.stream.Collectors; 95 | 96 | import javax.annotation.PostConstruct; 97 | 98 | import cn.gathub.auth.constant.MessageConstant; 99 | import cn.gathub.auth.domain.entity.User; 100 | import cn.gathub.auth.service.UserService; 101 | import cn.gathub.auth.service.principal.UserPrincipal; 102 | import cn.hutool.core.collection.CollUtil; 103 | 104 | /** 105 | * 用户管理业务类 106 | * 107 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 108 | */ 109 | @Service 110 | public class UserServiceImpl implements UserService { 111 | 112 | private List userList; 113 | private final PasswordEncoder passwordEncoder; 114 | 115 | public UserServiceImpl(PasswordEncoder passwordEncoder) { 116 | this.passwordEncoder = passwordEncoder; 117 | } 118 | 119 | @PostConstruct 120 | public void initData() { 121 | String password = passwordEncoder.encode("123456"); 122 | userList = new ArrayList<>(); 123 | userList.add(new User(1L, "admin", password, 1, CollUtil.toList("ADMIN"))); 124 | userList.add(new User(2L, "user", password, 1, CollUtil.toList("USER"))); 125 | } 126 | 127 | @Override 128 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 129 | List findUserList = userList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList()); 130 | if (CollUtil.isEmpty(findUserList)) { 131 | throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); 132 | } 133 | UserPrincipal userPrincipal = new UserPrincipal(findUserList.get(0)); 134 | if (!userPrincipal.isEnabled()) { 135 | throw new DisabledException(MessageConstant.ACCOUNT_DISABLED); 136 | } else if (!userPrincipal.isAccountNonLocked()) { 137 | throw new LockedException(MessageConstant.ACCOUNT_LOCKED); 138 | } else if (!userPrincipal.isAccountNonExpired()) { 139 | throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED); 140 | } else if (!userPrincipal.isCredentialsNonExpired()) { 141 | throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED); 142 | } 143 | return userPrincipal; 144 | } 145 | 146 | } 147 | 148 | ``` 149 | 150 | > 5、创建ClientServiceImpl类实现Spring Security的ClientDetailsService接口,用于加载客户端信息 151 | 152 | ```java 153 | package cn.gathub.auth.service.impl; 154 | 155 | import org.springframework.http.HttpStatus; 156 | import org.springframework.security.crypto.password.PasswordEncoder; 157 | import org.springframework.security.oauth2.provider.ClientDetails; 158 | import org.springframework.security.oauth2.provider.ClientRegistrationException; 159 | import org.springframework.stereotype.Service; 160 | import org.springframework.web.server.ResponseStatusException; 161 | 162 | import java.util.ArrayList; 163 | import java.util.List; 164 | import java.util.stream.Collectors; 165 | 166 | import javax.annotation.PostConstruct; 167 | 168 | import cn.gathub.auth.constant.MessageConstant; 169 | import cn.gathub.auth.domain.entity.Client; 170 | import cn.gathub.auth.service.ClientService; 171 | import cn.gathub.auth.service.principal.ClientPrincipal; 172 | import cn.hutool.core.collection.CollUtil; 173 | 174 | /** 175 | * 客户端管理业务类 176 | * 177 | * @author Honghui [wanghonghui_work@163.com] 2021/3/18 178 | */ 179 | @Service 180 | public class ClientServiceImpl implements ClientService { 181 | 182 | private List clientList; 183 | private final PasswordEncoder passwordEncoder; 184 | 185 | public ClientServiceImpl(PasswordEncoder passwordEncoder) { 186 | this.passwordEncoder = passwordEncoder; 187 | } 188 | 189 | @PostConstruct 190 | public void initData() { 191 | String clientSecret = passwordEncoder.encode("123456"); 192 | clientList = new ArrayList<>(); 193 | // 1、密码模式 194 | clientList.add(Client.builder() 195 | .clientId("client-app") 196 | .resourceIds("oauth2-resource") 197 | .secretRequire(false) 198 | .clientSecret(clientSecret) 199 | .scopeRequire(false) 200 | .scope("all") 201 | .authorizedGrantTypes("password,refresh_token") 202 | .authorities("ADMIN,USER") 203 | .accessTokenValidity(3600) 204 | .refreshTokenValidity(86400).build()); 205 | // 2、授权码模式 206 | clientList.add(Client.builder() 207 | .clientId("client-app-2") 208 | .resourceIds("oauth2-resource2") 209 | .secretRequire(false) 210 | .clientSecret(clientSecret) 211 | .scopeRequire(false) 212 | .scope("all") 213 | .authorizedGrantTypes("authorization_code,refresh_token") 214 | .webServerRedirectUri("https://www.gathub.cn,https://www.baidu.com") 215 | .authorities("USER") 216 | .accessTokenValidity(3600) 217 | .refreshTokenValidity(86400).build()); 218 | } 219 | 220 | @Override 221 | public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException { 222 | List findClientList = clientList.stream().filter(item -> item.getClientId().equals(clientId)).collect(Collectors.toList()); 223 | if (CollUtil.isEmpty(findClientList)) { 224 | throw new ResponseStatusException(HttpStatus.NOT_FOUND, MessageConstant.NOT_FOUND_CLIENT); 225 | } 226 | return new ClientPrincipal(findClientList.get(0)); 227 | } 228 | } 229 | 230 | ``` 231 | 232 | > 6、添加认证服务相关配置Oauth2ServerConfig,需要配置加载用户信息的服务UserServiceImpl和加载客户端信息的服务ClientServiceImpl及RSA的钥匙对KeyPair 233 | 234 | ```java 235 | package cn.gathub.auth.config; 236 | 237 | import org.springframework.context.annotation.Bean; 238 | import org.springframework.context.annotation.Configuration; 239 | import org.springframework.core.io.ClassPathResource; 240 | import org.springframework.security.authentication.AuthenticationManager; 241 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 242 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 243 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 244 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 245 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; 246 | import org.springframework.security.oauth2.provider.token.TokenEnhancer; 247 | import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; 248 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; 249 | import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; 250 | 251 | import java.security.KeyPair; 252 | import java.util.ArrayList; 253 | import java.util.List; 254 | 255 | import cn.gathub.auth.component.JwtTokenEnhancer; 256 | import cn.gathub.auth.service.ClientService; 257 | import cn.gathub.auth.service.UserService; 258 | import lombok.AllArgsConstructor; 259 | 260 | /** 261 | * 认证服务器配置 262 | * 263 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 264 | */ 265 | @AllArgsConstructor 266 | @Configuration 267 | @EnableAuthorizationServer 268 | public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter { 269 | 270 | private final UserService userService; 271 | private final ClientService clientService; 272 | private final AuthenticationManager authenticationManager; 273 | private final JwtTokenEnhancer jwtTokenEnhancer; 274 | 275 | @Override 276 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 277 | // clients.inMemory() 278 | // // 1、密码模式 279 | // .withClient("client-app") 280 | // .secret(passwordEncoder.encode("123456")) 281 | // .scopes("read,write") 282 | // .authorizedGrantTypes("password", "refresh_token") 283 | // .accessTokenValiditySeconds(3600) 284 | // .refreshTokenValiditySeconds(86400) 285 | // .and() 286 | // // 2、授权码授权 287 | // .withClient("client-app-2") 288 | // .secret(passwordEncoder.encode("123456")) 289 | // .scopes("read") 290 | // .authorizedGrantTypes("authorization_code", "refresh_token") 291 | // .accessTokenValiditySeconds(3600) 292 | // .refreshTokenValiditySeconds(86400) 293 | // .redirectUris("https://www.gathub.cn", "https://www.baidu.com"); 294 | clients.withClientDetails(clientService); 295 | } 296 | 297 | @Override 298 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) { 299 | TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); 300 | List delegates = new ArrayList<>(); 301 | delegates.add(jwtTokenEnhancer); 302 | delegates.add(accessTokenConverter()); 303 | enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器 304 | endpoints.authenticationManager(authenticationManager) 305 | .userDetailsService(userService) //配置加载用户信息的服务 306 | .accessTokenConverter(accessTokenConverter()) 307 | .tokenEnhancer(enhancerChain); 308 | } 309 | 310 | @Override 311 | public void configure(AuthorizationServerSecurityConfigurer security) { 312 | security.allowFormAuthenticationForClients(); 313 | } 314 | 315 | @Bean 316 | public JwtAccessTokenConverter accessTokenConverter() { 317 | JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); 318 | jwtAccessTokenConverter.setKeyPair(keyPair()); 319 | return jwtAccessTokenConverter; 320 | } 321 | 322 | @Bean 323 | public KeyPair keyPair() { 324 | // 从classpath下的证书中获取秘钥对 325 | KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "654321".toCharArray()); 326 | return keyStoreKeyFactory.getKeyPair("jwt", "654321".toCharArray()); 327 | } 328 | 329 | } 330 | 331 | ``` 332 | 333 | > 7、如果你想往JWT中添加自定义信息的话,比如说登录用户的ID,可以自己实现TokenEnhancer接口 334 | 335 | ```java 336 | package cn.gathub.auth.component; 337 | 338 | import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; 339 | import org.springframework.security.oauth2.common.OAuth2AccessToken; 340 | import org.springframework.security.oauth2.provider.OAuth2Authentication; 341 | import org.springframework.security.oauth2.provider.token.TokenEnhancer; 342 | import org.springframework.stereotype.Component; 343 | 344 | import java.util.HashMap; 345 | import java.util.Map; 346 | 347 | import cn.gathub.auth.service.principal.UserPrincipal; 348 | 349 | 350 | /** 351 | * JWT内容增强器 352 | * 353 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 354 | */ 355 | @Component 356 | public class JwtTokenEnhancer implements TokenEnhancer { 357 | @Override 358 | public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { 359 | UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); 360 | Map info = new HashMap<>(); 361 | // 把用户ID设置到JWT中 362 | info.put("id", userPrincipal.getId()); 363 | ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); 364 | return accessToken; 365 | } 366 | } 367 | 368 | ``` 369 | 370 | > 8、由于我们的网关服务需要RSA的公钥来验证签名是否合法,所以认证服务需要有个接口把公钥暴露出来 371 | 372 | ```java 373 | package cn.gathub.auth.controller; 374 | 375 | import com.nimbusds.jose.jwk.JWKSet; 376 | import com.nimbusds.jose.jwk.RSAKey; 377 | 378 | import org.springframework.web.bind.annotation.GetMapping; 379 | import org.springframework.web.bind.annotation.RestController; 380 | 381 | import java.security.KeyPair; 382 | import java.security.interfaces.RSAPublicKey; 383 | import java.util.Map; 384 | 385 | /** 386 | * 获取RSA公钥接口 387 | * 388 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 389 | */ 390 | @RestController 391 | public class KeyPairController { 392 | 393 | private final KeyPair keyPair; 394 | 395 | public KeyPairController(KeyPair keyPair) { 396 | this.keyPair = keyPair; 397 | } 398 | 399 | @GetMapping("/rsa/publicKey") 400 | public Map getKey() { 401 | RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); 402 | RSAKey key = new RSAKey.Builder(publicKey).build(); 403 | return new JWKSet(key).toJSONObject(); 404 | } 405 | 406 | } 407 | 408 | ``` 409 | 410 | > 9、还需要配置Spring Security,允许获取公钥接口的访问 411 | 412 | ```java 413 | package cn.gathub.auth.config; 414 | 415 | import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; 416 | import org.springframework.context.annotation.Bean; 417 | import org.springframework.context.annotation.Configuration; 418 | import org.springframework.security.authentication.AuthenticationManager; 419 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 420 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 421 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 422 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 423 | import org.springframework.security.crypto.password.PasswordEncoder; 424 | 425 | /** 426 | * SpringSecurity配置 427 | * 428 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 429 | */ 430 | @Configuration 431 | @EnableWebSecurity 432 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 433 | 434 | @Override 435 | protected void configure(HttpSecurity http) throws Exception { 436 | http.authorizeRequests() 437 | .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() 438 | .antMatchers("/rsa/publicKey").permitAll() 439 | .anyRequest().authenticated(); 440 | } 441 | 442 | @Bean 443 | @Override 444 | public AuthenticationManager authenticationManagerBean() throws Exception { 445 | return super.authenticationManagerBean(); 446 | } 447 | 448 | @Bean 449 | public PasswordEncoder passwordEncoder() { 450 | return new BCryptPasswordEncoder(); 451 | } 452 | 453 | } 454 | 455 | ``` 456 | 457 | > 10、创建一个资源服务ResourceServiceImpl,初始化的时候把资源与角色匹配关系缓存到Redis中,方便网关服务进行鉴权的时候获取 458 | 459 | ```java 460 | package cn.gathub.auth.service; 461 | 462 | import org.springframework.data.redis.core.RedisTemplate; 463 | import org.springframework.stereotype.Service; 464 | 465 | import java.util.List; 466 | import java.util.Map; 467 | import java.util.TreeMap; 468 | 469 | import javax.annotation.PostConstruct; 470 | 471 | import cn.gathub.auth.constant.RedisConstant; 472 | import cn.hutool.core.collection.CollUtil; 473 | 474 | /** 475 | * 资源与角色匹配关系管理业务类 476 | * 477 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 478 | */ 479 | @Service 480 | public class ResourceServiceImpl { 481 | 482 | private final RedisTemplate redisTemplate; 483 | 484 | public ResourceServiceImpl(RedisTemplate redisTemplate) { 485 | this.redisTemplate = redisTemplate; 486 | } 487 | 488 | @PostConstruct 489 | public void initData() { 490 | Map> resourceRolesMap = new TreeMap<>(); 491 | resourceRolesMap.put("/resource/hello", CollUtil.toList("ADMIN")); 492 | resourceRolesMap.put("/resource/user/currentUser", CollUtil.toList("ADMIN", "USER")); 493 | redisTemplate.opsForHash().putAll(RedisConstant.RESOURCE_ROLES_MAP, resourceRolesMap); 494 | } 495 | } 496 | 497 | ``` 498 | 499 | ### 二、网关服务`oauth2-gateway` 500 | 接下来搭建网关服务,它将作为Oauth2的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作 501 | 502 | > 1、在pom.xml中添加相关依赖,主要是Gateway、Oauth2和JWT相关依赖 503 | 504 | ```java 505 | 506 | 507 | org.springframework.boot 508 | spring-boot-starter-webflux 509 | 510 | 511 | org.springframework.cloud 512 | spring-cloud-starter-gateway 513 | 514 | 515 | org.springframework.security 516 | spring-security-config 517 | 518 | 519 | org.springframework.security 520 | spring-security-oauth2-resource-server 521 | 522 | 523 | org.springframework.security 524 | spring-security-oauth2-client 525 | 526 | 527 | org.springframework.security 528 | spring-security-oauth2-jose 529 | 530 | 531 | com.nimbusds 532 | nimbus-jose-jwt 533 | 8.16 534 | 535 | 536 | 537 | ``` 538 | 539 | > 2、在application.yml中添加相关配置,主要是路由规则的配置、Oauth2中RSA公钥的配置及路由白名单的配置 540 | 541 | ```yml 542 | server: 543 | port: 9201 544 | spring: 545 | profiles: 546 | active: dev 547 | application: 548 | name: oauth2-gateway 549 | cloud: 550 | nacos: 551 | discovery: 552 | server-addr: localhost:8848 553 | gateway: 554 | routes: # 配置路由路径 555 | - id: oauth2-resource-route 556 | uri: lb://oauth2-resource 557 | predicates: 558 | - Path=/resource/** 559 | filters: 560 | - StripPrefix=1 561 | - id: oauth2-auth-route 562 | uri: lb://oauth2-auth 563 | predicates: 564 | - Path=/auth/** 565 | filters: 566 | - StripPrefix=1 567 | - id: oauth2-auth-login 568 | uri: lb://oauth2-auth 569 | predicates: 570 | - Path=/login 571 | filters: 572 | - PreserveHostHeader 573 | - id: oauth2-auth-token 574 | uri: lb://oauth2-auth 575 | predicates: 576 | - Path=/oauth/token 577 | filters: 578 | - PreserveHostHeader 579 | - id: oauth2-auth-authorize 580 | uri: lb://oauth2-auth 581 | predicates: 582 | - Path=/oauth/authorize 583 | filters: 584 | - PreserveHostHeader 585 | discovery: 586 | locator: 587 | enabled: true # 开启从注册中心动态创建路由的功能 588 | lower-case-service-id: true # 使用小写服务名,默认是大写 589 | security: 590 | oauth2: 591 | resourceserver: 592 | jwt: 593 | jwk-set-uri: 'http://localhost:9401/rsa/publicKey' # 配置RSA的公钥访问地址 594 | redis: 595 | database: 0 596 | port: 6379 597 | host: localhost 598 | password: 599 | secure: 600 | ignore: 601 | urls: # 配置白名单路径 602 | - "/actuator/**" 603 | - "/oauth/token" 604 | - "/oauth/authorize" 605 | - "/login" 606 | 607 | ``` 608 | 609 | > 3、对网关服务进行配置安全配置,由于Gateway使用的是WebFlux,所以需要使用@EnableWebFluxSecurity注解开启 610 | 611 | ```java 612 | package cn.gathub.gateway.config; 613 | 614 | 615 | import org.springframework.context.annotation.Bean; 616 | import org.springframework.context.annotation.Configuration; 617 | import org.springframework.core.convert.converter.Converter; 618 | import org.springframework.security.authentication.AbstractAuthenticationToken; 619 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 620 | import org.springframework.security.config.web.server.SecurityWebFiltersOrder; 621 | import org.springframework.security.config.web.server.ServerHttpSecurity; 622 | import org.springframework.security.oauth2.jwt.Jwt; 623 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; 624 | import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; 625 | import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; 626 | import org.springframework.security.web.server.SecurityWebFilterChain; 627 | 628 | import cn.gathub.gateway.authorization.AuthorizationManager; 629 | import cn.gathub.gateway.component.RestAuthenticationEntryPoint; 630 | import cn.gathub.gateway.component.RestfulAccessDeniedHandler; 631 | import cn.gathub.gateway.constant.AuthConstant; 632 | import cn.gathub.gateway.filter.IgnoreUrlsRemoveJwtFilter; 633 | import cn.hutool.core.util.ArrayUtil; 634 | import lombok.AllArgsConstructor; 635 | import reactor.core.publisher.Mono; 636 | 637 | /** 638 | * 资源服务器配置 639 | * 640 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 641 | */ 642 | @AllArgsConstructor 643 | @Configuration 644 | @EnableWebFluxSecurity 645 | public class ResourceServerConfig { 646 | private final AuthorizationManager authorizationManager; 647 | private final IgnoreUrlsConfig ignoreUrlsConfig; 648 | private final RestfulAccessDeniedHandler restfulAccessDeniedHandler; 649 | private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; 650 | private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter; 651 | 652 | @Bean 653 | public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { 654 | http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter()); 655 | // 1、自定义处理JWT请求头过期或签名错误的结果 656 | http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint); 657 | // 2、对白名单路径,直接移除JWT请求头 658 | http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION); 659 | http.authorizeExchange() 660 | .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(), String.class)).permitAll() // 白名单配置 661 | .anyExchange().access(authorizationManager) // 鉴权管理器配置 662 | .and().exceptionHandling() 663 | .accessDeniedHandler(restfulAccessDeniedHandler) // 处理未授权 664 | .authenticationEntryPoint(restAuthenticationEntryPoint) // 处理未认证 665 | .and().csrf().disable(); 666 | return http.build(); 667 | } 668 | 669 | @Bean 670 | public Converter> jwtAuthenticationConverter() { 671 | JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); 672 | jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX); 673 | jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME); 674 | JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); 675 | jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); 676 | return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); 677 | } 678 | 679 | } 680 | 681 | ``` 682 | > 4、在WebFluxSecurity中自定义鉴权操作需要实现ReactiveAuthorizationManager接口 683 | 684 | ```java 685 | package cn.gathub.gateway.authorization; 686 | 687 | 688 | import org.springframework.data.redis.core.RedisTemplate; 689 | import org.springframework.security.authorization.AuthorizationDecision; 690 | import org.springframework.security.authorization.ReactiveAuthorizationManager; 691 | import org.springframework.security.core.Authentication; 692 | import org.springframework.security.core.GrantedAuthority; 693 | import org.springframework.security.web.server.authorization.AuthorizationContext; 694 | import org.springframework.stereotype.Component; 695 | 696 | import java.net.URI; 697 | import java.util.List; 698 | import java.util.stream.Collectors; 699 | 700 | import cn.gathub.gateway.constant.AuthConstant; 701 | import cn.gathub.gateway.constant.RedisConstant; 702 | import cn.hutool.core.convert.Convert; 703 | import reactor.core.publisher.Mono; 704 | 705 | /** 706 | * 鉴权管理器,用于判断是否有资源的访问权限 707 | * 708 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 709 | */ 710 | @Component 711 | public class AuthorizationManager implements ReactiveAuthorizationManager { 712 | private final RedisTemplate redisTemplate; 713 | 714 | public AuthorizationManager(RedisTemplate redisTemplate) { 715 | this.redisTemplate = redisTemplate; 716 | } 717 | 718 | @Override 719 | public Mono check(Mono mono, AuthorizationContext authorizationContext) { 720 | // 1、从Redis中获取当前路径可访问角色列表 721 | URI uri = authorizationContext.getExchange().getRequest().getURI(); 722 | Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath()); 723 | List authorities = Convert.toList(String.class, obj); 724 | authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList()); 725 | // 2、认证通过且角色匹配的用户可访问当前路径 726 | return mono 727 | .filter(Authentication::isAuthenticated) 728 | .flatMapIterable(Authentication::getAuthorities) 729 | .map(GrantedAuthority::getAuthority) 730 | .any(authorities::contains) 731 | .map(AuthorizationDecision::new) 732 | .defaultIfEmpty(new AuthorizationDecision(false)); 733 | } 734 | 735 | } 736 | 737 | ``` 738 | 739 | > 5、这里我们还需要实现一个全局过滤器AuthGlobalFilter,当鉴权通过后将JWT令牌中的用户信息解析出来,然后存入请求的Header中,这样后续服务就不需要解析JWT令牌了,可以直接从请求的Header中获取到用户信息 740 | 741 | ```java 742 | package cn.gathub.gateway.filter; 743 | 744 | import com.nimbusds.jose.JWSObject; 745 | 746 | import org.slf4j.Logger; 747 | import org.slf4j.LoggerFactory; 748 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 749 | import org.springframework.cloud.gateway.filter.GlobalFilter; 750 | import org.springframework.core.Ordered; 751 | import org.springframework.http.server.reactive.ServerHttpRequest; 752 | import org.springframework.stereotype.Component; 753 | import org.springframework.web.server.ServerWebExchange; 754 | 755 | import java.text.ParseException; 756 | 757 | import cn.hutool.core.util.StrUtil; 758 | import reactor.core.publisher.Mono; 759 | 760 | /** 761 | * 将登录用户的JWT转化成用户信息的全局过滤器 762 | * 763 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 764 | */ 765 | @Component 766 | public class AuthGlobalFilter implements GlobalFilter, Ordered { 767 | 768 | private final static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class); 769 | 770 | @Override 771 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { 772 | String token = exchange.getRequest().getHeaders().getFirst("Authorization"); 773 | if (StrUtil.isEmpty(token)) { 774 | return chain.filter(exchange); 775 | } 776 | try { 777 | // 从token中解析用户信息并设置到Header中去 778 | String realToken = token.replace("Bearer ", ""); 779 | JWSObject jwsObject = JWSObject.parse(realToken); 780 | String userStr = jwsObject.getPayload().toString(); 781 | LOGGER.info("AuthGlobalFilter.filter() user:{}", userStr); 782 | ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build(); 783 | exchange = exchange.mutate().request(request).build(); 784 | } catch (ParseException e) { 785 | e.printStackTrace(); 786 | } 787 | return chain.filter(exchange); 788 | } 789 | 790 | @Override 791 | public int getOrder() { 792 | return 0; 793 | } 794 | } 795 | 796 | ``` 797 | 798 | ### 三、资源服务(API服务)`oauth2-resource` 799 | 800 | 最后我们搭建一个API服务,它不会集成和实现任何安全相关逻辑,全靠网关来保护它 801 | 802 | > 1、在pom.xml中添加相关依赖,就添加了一个web依赖 803 | 804 | ```java 805 | 806 | 807 | org.springframework.boot 808 | spring-boot-starter-web 809 | 810 | 811 | ``` 812 | 813 | > 2、在application.yml添加相关配置,很常规的配置 814 | 815 | ```yml 816 | server: 817 | port: 9501 818 | spring: 819 | profiles: 820 | active: dev 821 | application: 822 | name: oauth2-resource 823 | cloud: 824 | nacos: 825 | discovery: 826 | server-addr: localhost:8848 827 | management: 828 | endpoints: 829 | web: 830 | exposure: 831 | include: "*" 832 | 833 | ``` 834 | 835 | > 3、创建一个测试接口,网关验证通过即可访问 836 | 837 | ```java 838 | package cn.gathub.resource.controller; 839 | 840 | import org.springframework.web.bind.annotation.GetMapping; 841 | import org.springframework.web.bind.annotation.RestController; 842 | 843 | /** 844 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 845 | */ 846 | @RestController 847 | public class HelloController { 848 | 849 | @GetMapping("/hello") 850 | public String hello() { 851 | return "Hello World !"; 852 | } 853 | 854 | } 855 | 856 | ``` 857 | 858 | > 4、创建一个获取登录中的用户信息的接口,用于从请求的Header中直接获取登录用户信息 859 | 860 | ```java 861 | package cn.gathub.resource.controller; 862 | 863 | 864 | import org.springframework.web.bind.annotation.GetMapping; 865 | import org.springframework.web.bind.annotation.RequestMapping; 866 | import org.springframework.web.bind.annotation.RestController; 867 | 868 | import javax.servlet.http.HttpServletRequest; 869 | 870 | import cn.gathub.resource.domain.User; 871 | import cn.hutool.core.convert.Convert; 872 | import cn.hutool.json.JSONObject; 873 | 874 | /** 875 | * 获取登录用户信息接口 876 | * 877 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 878 | */ 879 | @RestController 880 | @RequestMapping("/user") 881 | public class UserController { 882 | 883 | @GetMapping("/currentUser") 884 | public User currentUser(HttpServletRequest request) { 885 | // 从Header中获取用户信息 886 | String userStr = request.getHeader("user"); 887 | JSONObject userJsonObject = new JSONObject(userStr); 888 | return User.builder() 889 | .username(userJsonObject.getStr("user_name")) 890 | .id(Convert.toLong(userJsonObject.get("id"))) 891 | .roles(Convert.toList(String.class, userJsonObject.get("authorities"))).build(); 892 | } 893 | } 894 | 895 | ``` 896 | 897 | ## 功能演示 898 | 在此之前先启动我们的 Nacos 和 Redis 服务,然后依次启动`oauth2-auth`、`oauth2-gateway`及`oauth2-api`服务 899 | 900 | 我这里测试使用的 Docker 跑的单机版的 Nacos 901 | ```shell 902 | docker pull nacos/nacos-server 903 | docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server 904 | ``` 905 | > 1、使用密码模式获取JWT令牌,访问地址:http://localhost:9201/oauth/token 906 | 907 | ![image](https://user-images.githubusercontent.com/35522446/111894792-b8583900-8a48-11eb-8206-57aeb76d25ab.png) 908 | 909 | > 2、使用获取到的JWT令牌访问需要权限的接口,访问地址:http://localhost:9201/resource/hello 910 | 911 | ![image](https://user-images.githubusercontent.com/35522446/111894802-d4f47100-8a48-11eb-9f78-9125d27e4cb3.png) 912 | 913 | > 3、使用获取到的JWT令牌访问获取当前登录用户信息的接口,访问地址:http://localhost:9201/resource/user/currentUser 914 | 915 | ![image](https://user-images.githubusercontent.com/35522446/111894819-fc4b3e00-8a48-11eb-853c-9ae1c58e4f18.png) 916 | 917 | > 4、当token不存在时,访问地址:http://localhost:9201/resource/user/currentUser 918 | 919 | ![image](https://user-images.githubusercontent.com/35522446/111894829-108f3b00-8a49-11eb-8460-cd936b7b15f3.png) 920 | 921 | > 5、当JWT令牌过期时,使用refresh_token获取新的JWT令牌,访问地址:http://localhost:9201/oauth/token 922 | 923 | ![image](https://user-images.githubusercontent.com/35522446/111894845-30befa00-8a49-11eb-8e35-878dada90401.png) 924 | 925 | > 6、使用授码模式登录时,先访问地址获取授权码:http://localhost:9201/oauth/authorize?response_type=code&client_id=client-app-2&redirect_uri=https://www.baidu.com 926 | 927 | > 7、访问地址,跳转登录页面 928 | 929 | ![image](https://user-images.githubusercontent.com/35522446/111894879-78458600-8a49-11eb-9de8-05acf802c212.png) 930 | 931 | > 8、登录成功,进入授权页面 932 | 933 | ![image](https://user-images.githubusercontent.com/35522446/111894893-9a3f0880-8a49-11eb-90fd-432717e88ac5.png) 934 | 935 | > 9、通过授权,拿到授权码 936 | 937 | ![image](https://user-images.githubusercontent.com/35522446/111894917-d6726900-8a49-11eb-9a78-0103ae6d2033.png) 938 | 939 | > 10、拿到授权码,访问地址登录:http://localhost:9201/oauth/token 940 | 941 | ![image](https://user-images.githubusercontent.com/35522446/111894933-fefa6300-8a49-11eb-8fd4-62c8ef9775f8.png) 942 | 943 | > 11、使用没有访问权限的`user`账号登录,访问接口时会返回如下信息,访问地址:http://localhost:9201/resource/hello 944 | 945 | ![image](https://user-images.githubusercontent.com/35522446/111894957-28b38a00-8a4a-11eb-8077-a159b8f6eef1.png) 946 | 947 | 948 | ## 项目源码地址 949 | https://github.com/it-wwh/spring-cloud-gateway-oauth2 950 | ## 公众号 951 | ![image](https://user-images.githubusercontent.com/35522446/111441584-69f22400-8742-11eb-8ca6-617554f54605.png) 952 | 953 | 954 | 955 | 956 | 957 | 958 | -------------------------------------------------------------------------------- /oauth2-auth/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | gathub-oauth2 7 | cn.gathub 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | oauth2-auth 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-web 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-security 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-starter-oauth2 26 | 27 | 28 | com.nimbusds 29 | nimbus-jose-jwt 30 | 8.16 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-data-redis 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/Oauth2AuthApplication.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | /** 8 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 9 | */ 10 | @EnableDiscoveryClient 11 | @SpringBootApplication 12 | public class Oauth2AuthApplication { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(Oauth2AuthApplication.class, args); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/api/CommonResult.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.api; 2 | 3 | /** 4 | * 通用返回对象 5 | * 6 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 7 | */ 8 | public class CommonResult { 9 | private long code; 10 | private String message; 11 | private T data; 12 | 13 | protected CommonResult() { 14 | } 15 | 16 | protected CommonResult(long code, String message, T data) { 17 | this.code = code; 18 | this.message = message; 19 | this.data = data; 20 | } 21 | 22 | /** 23 | * 成功返回结果 24 | * 25 | * @param data 获取的数据 26 | */ 27 | public static CommonResult success(T data) { 28 | return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); 29 | } 30 | 31 | /** 32 | * 成功返回结果 33 | * 34 | * @param data 获取的数据 35 | * @param message 提示信息 36 | */ 37 | public static CommonResult success(T data, String message) { 38 | return new CommonResult(ResultCode.SUCCESS.getCode(), message, data); 39 | } 40 | 41 | /** 42 | * 失败返回结果 43 | * 44 | * @param errorCode 错误码 45 | */ 46 | public static CommonResult failed(IErrorCode errorCode) { 47 | return new CommonResult(errorCode.getCode(), errorCode.getMessage(), null); 48 | } 49 | 50 | /** 51 | * 失败返回结果 52 | * 53 | * @param errorCode 错误码 54 | * @param message 错误信息 55 | */ 56 | public static CommonResult failed(IErrorCode errorCode, String message) { 57 | return new CommonResult(errorCode.getCode(), message, null); 58 | } 59 | 60 | /** 61 | * 失败返回结果 62 | * 63 | * @param message 提示信息 64 | */ 65 | public static CommonResult failed(String message) { 66 | return new CommonResult(ResultCode.FAILED.getCode(), message, null); 67 | } 68 | 69 | /** 70 | * 失败返回结果 71 | */ 72 | public static CommonResult failed() { 73 | return failed(ResultCode.FAILED); 74 | } 75 | 76 | /** 77 | * 参数验证失败返回结果 78 | */ 79 | public static CommonResult validateFailed() { 80 | return failed(ResultCode.VALIDATE_FAILED); 81 | } 82 | 83 | /** 84 | * 参数验证失败返回结果 85 | * 86 | * @param message 提示信息 87 | */ 88 | public static CommonResult validateFailed(String message) { 89 | return new CommonResult(ResultCode.VALIDATE_FAILED.getCode(), message, null); 90 | } 91 | 92 | /** 93 | * 未登录返回结果 94 | */ 95 | public static CommonResult unauthorized(T data) { 96 | return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); 97 | } 98 | 99 | /** 100 | * 未授权返回结果 101 | */ 102 | public static CommonResult forbidden(T data) { 103 | return new CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); 104 | } 105 | 106 | public long getCode() { 107 | return code; 108 | } 109 | 110 | public void setCode(long code) { 111 | this.code = code; 112 | } 113 | 114 | public String getMessage() { 115 | return message; 116 | } 117 | 118 | public void setMessage(String message) { 119 | this.message = message; 120 | } 121 | 122 | public T getData() { 123 | return data; 124 | } 125 | 126 | public void setData(T data) { 127 | this.data = data; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/api/IErrorCode.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.api; 2 | 3 | /** 4 | * 封装API的错误码 5 | * 6 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 7 | */ 8 | public interface IErrorCode { 9 | long getCode(); 10 | 11 | String getMessage(); 12 | } 13 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/api/ResultCode.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.api; 2 | 3 | /** 4 | * 枚举了一些常用API操作码 5 | * 6 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 7 | */ 8 | public enum ResultCode implements IErrorCode { 9 | SUCCESS(200, "操作成功"), 10 | FAILED(500, "操作失败"), 11 | VALIDATE_FAILED(404, "参数检验失败"), 12 | UNAUTHORIZED(401, "暂未登录或token已经过期"), 13 | FORBIDDEN(403, "没有相关权限"); 14 | private final long code; 15 | private final String message; 16 | 17 | ResultCode(long code, String message) { 18 | this.code = code; 19 | this.message = message; 20 | } 21 | 22 | public long getCode() { 23 | return code; 24 | } 25 | 26 | public String getMessage() { 27 | return message; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/component/JwtTokenEnhancer.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.component; 2 | 3 | import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; 4 | import org.springframework.security.oauth2.common.OAuth2AccessToken; 5 | import org.springframework.security.oauth2.provider.OAuth2Authentication; 6 | import org.springframework.security.oauth2.provider.token.TokenEnhancer; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import cn.gathub.auth.service.principal.UserPrincipal; 13 | 14 | 15 | /** 16 | * JWT内容增强器 17 | * 18 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 19 | */ 20 | @Component 21 | public class JwtTokenEnhancer implements TokenEnhancer { 22 | @Override 23 | public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { 24 | UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); 25 | Map info = new HashMap<>(); 26 | // 把用户ID设置到JWT中 27 | info.put("id", userPrincipal.getId()); 28 | ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); 29 | return accessToken; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/config/Oauth2ServerConfig.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.core.io.ClassPathResource; 6 | import org.springframework.security.authentication.AuthenticationManager; 7 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 8 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 9 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 10 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 11 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; 12 | import org.springframework.security.oauth2.provider.token.TokenEnhancer; 13 | import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; 14 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; 15 | import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; 16 | 17 | import java.security.KeyPair; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import cn.gathub.auth.component.JwtTokenEnhancer; 22 | import cn.gathub.auth.service.ClientService; 23 | import cn.gathub.auth.service.UserService; 24 | import lombok.AllArgsConstructor; 25 | 26 | /** 27 | * 认证服务器配置 28 | * 29 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 30 | */ 31 | @AllArgsConstructor 32 | @Configuration 33 | @EnableAuthorizationServer 34 | public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter { 35 | 36 | private final UserService userService; 37 | private final ClientService clientService; 38 | private final AuthenticationManager authenticationManager; 39 | private final JwtTokenEnhancer jwtTokenEnhancer; 40 | 41 | @Override 42 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 43 | // clients.inMemory() 44 | // // 1、密码模式 45 | // .withClient("client-app") 46 | // .secret(passwordEncoder.encode("123456")) 47 | // .scopes("read,write") 48 | // .authorizedGrantTypes("password", "refresh_token") 49 | // .accessTokenValiditySeconds(3600) 50 | // .refreshTokenValiditySeconds(86400) 51 | // .and() 52 | // // 2、授权码授权 53 | // .withClient("client-app-2") 54 | // .secret(passwordEncoder.encode("123456")) 55 | // .scopes("read") 56 | // .authorizedGrantTypes("authorization_code", "refresh_token") 57 | // .accessTokenValiditySeconds(3600) 58 | // .refreshTokenValiditySeconds(86400) 59 | // .redirectUris("https://www.gathub.cn", "https://www.baidu.com"); 60 | clients.withClientDetails(clientService); 61 | } 62 | 63 | @Override 64 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) { 65 | TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); 66 | List delegates = new ArrayList<>(); 67 | delegates.add(jwtTokenEnhancer); 68 | delegates.add(accessTokenConverter()); 69 | enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器 70 | endpoints.authenticationManager(authenticationManager) 71 | .userDetailsService(userService) //配置加载用户信息的服务 72 | .accessTokenConverter(accessTokenConverter()) 73 | .tokenEnhancer(enhancerChain); 74 | } 75 | 76 | @Override 77 | public void configure(AuthorizationServerSecurityConfigurer security) { 78 | security.allowFormAuthenticationForClients(); 79 | } 80 | 81 | @Bean 82 | public JwtAccessTokenConverter accessTokenConverter() { 83 | JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); 84 | jwtAccessTokenConverter.setKeyPair(keyPair()); 85 | return jwtAccessTokenConverter; 86 | } 87 | 88 | @Bean 89 | public KeyPair keyPair() { 90 | // 从classpath下的证书中获取秘钥对 91 | KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "654321".toCharArray()); 92 | return keyStoreKeyFactory.getKeyPair("jwt", "654321".toCharArray()); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/config/RedisRepositoryConfig.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.redis.connection.RedisConnectionFactory; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; 8 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 9 | import org.springframework.data.redis.serializer.StringRedisSerializer; 10 | 11 | /** 12 | * Redis相关配置 13 | * 14 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 15 | */ 16 | @Configuration 17 | @EnableRedisRepositories 18 | public class RedisRepositoryConfig { 19 | 20 | @Bean 21 | public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { 22 | RedisTemplate redisTemplate = new RedisTemplate<>(); 23 | redisTemplate.setConnectionFactory(connectionFactory); 24 | StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); 25 | redisTemplate.setKeySerializer(stringRedisSerializer); 26 | redisTemplate.setHashKeySerializer(stringRedisSerializer); 27 | Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); 28 | redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); 29 | redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); 30 | redisTemplate.afterPropertiesSet(); 31 | return redisTemplate; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.config; 2 | 3 | import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.authentication.AuthenticationManager; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 10 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | 13 | /** 14 | * SpringSecurity配置 15 | * 16 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 17 | */ 18 | @Configuration 19 | @EnableWebSecurity 20 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 21 | 22 | @Override 23 | protected void configure(HttpSecurity http) throws Exception { 24 | http.authorizeRequests() 25 | .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() 26 | .antMatchers("/rsa/publicKey").permitAll() 27 | .anyRequest().authenticated() 28 | .and().formLogin().permitAll(); 29 | } 30 | 31 | @Bean 32 | @Override 33 | public AuthenticationManager authenticationManagerBean() throws Exception { 34 | return super.authenticationManagerBean(); 35 | } 36 | 37 | @Bean 38 | public PasswordEncoder passwordEncoder() { 39 | return new BCryptPasswordEncoder(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/constant/MessageConstant.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.constant; 2 | 3 | /** 4 | * 消息常量 5 | * 6 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 7 | */ 8 | public class MessageConstant { 9 | 10 | public static final String SPLIT_COMMA = ","; 11 | public static final String LOGIN_SUCCESS = "登录成功!"; 12 | public static final String USERNAME_PASSWORD_ERROR = "用户名或密码错误!"; 13 | public static final String CREDENTIALS_EXPIRED = "该账户的登录凭证已过期,请重新登录!"; 14 | public static final String ACCOUNT_DISABLED = "该账户已被禁用,请联系管理员!"; 15 | public static final String ACCOUNT_LOCKED = "该账号已被锁定,请联系管理员!"; 16 | public static final String ACCOUNT_EXPIRED = "该账号已过期,请联系管理员!"; 17 | public static final String PERMISSION_DENIED = "没有访问权限,请联系管理员!"; 18 | public static final String NOT_FOUND_CLIENT = "未找到客户端!"; 19 | } 20 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/constant/RedisConstant.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.constant; 2 | 3 | /** 4 | * Redis常量 5 | * 6 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 7 | */ 8 | public class RedisConstant { 9 | 10 | public static final String RESOURCE_ROLES_MAP = "AUTH:RESOURCE_ROLES_MAP"; 11 | } 12 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.controller; 2 | 3 | import org.springframework.security.oauth2.common.OAuth2AccessToken; 4 | import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; 5 | import org.springframework.web.HttpRequestMethodNotSupportedException; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.security.Principal; 12 | import java.util.Map; 13 | import java.util.Objects; 14 | 15 | import cn.gathub.auth.api.CommonResult; 16 | import cn.gathub.auth.domain.dto.Oauth2TokenDto; 17 | 18 | /** 19 | * 自定义Oauth2获取令牌接口 20 | * 21 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 22 | */ 23 | @RestController 24 | @RequestMapping("/oauth") 25 | public class AuthController { 26 | 27 | private final TokenEndpoint tokenEndpoint; 28 | 29 | public AuthController(TokenEndpoint tokenEndpoint) { 30 | this.tokenEndpoint = tokenEndpoint; 31 | } 32 | 33 | /** 34 | * Oauth2登录认证 35 | */ 36 | @PostMapping("/token") 37 | public CommonResult postAccessToken(Principal principal, @RequestParam Map parameters) throws HttpRequestMethodNotSupportedException { 38 | OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); 39 | Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder() 40 | .token(Objects.requireNonNull(oAuth2AccessToken).getValue()) 41 | .refreshToken(oAuth2AccessToken.getRefreshToken().getValue()) 42 | .expiresIn(oAuth2AccessToken.getExpiresIn()) 43 | .tokenHead("Bearer ").build(); 44 | return CommonResult.success(oauth2TokenDto); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/controller/KeyPairController.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.controller; 2 | 3 | import com.nimbusds.jose.jwk.JWKSet; 4 | import com.nimbusds.jose.jwk.RSAKey; 5 | 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import java.security.KeyPair; 10 | import java.security.interfaces.RSAPublicKey; 11 | import java.util.Map; 12 | 13 | /** 14 | * 获取RSA公钥接口 15 | * 16 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 17 | */ 18 | @RestController 19 | public class KeyPairController { 20 | 21 | private final KeyPair keyPair; 22 | 23 | public KeyPairController(KeyPair keyPair) { 24 | this.keyPair = keyPair; 25 | } 26 | 27 | @GetMapping("/rsa/publicKey") 28 | public Map getKey() { 29 | RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); 30 | RSAKey key = new RSAKey.Builder(publicKey).build(); 31 | return new JWKSet(key).toJSONObject(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/domain/dto/Oauth2TokenDto.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.domain.dto; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | /** 8 | * Oauth2获取Token返回信息封装 9 | * 10 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 11 | */ 12 | @Data 13 | @EqualsAndHashCode(callSuper = false) 14 | @Builder 15 | public class Oauth2TokenDto { 16 | /** 17 | * 访问令牌 18 | */ 19 | private String token; 20 | /** 21 | * 刷新令牌 22 | */ 23 | private String refreshToken; 24 | /** 25 | * 访问令牌头前缀 26 | */ 27 | private String tokenHead; 28 | /** 29 | * 有效时间(秒) 30 | */ 31 | private int expiresIn; 32 | } 33 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/domain/entity/Client.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.domain.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.EqualsAndHashCode; 7 | 8 | /** 9 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 10 | */ 11 | @Data 12 | @EqualsAndHashCode(callSuper = false) 13 | @AllArgsConstructor 14 | @Builder(toBuilder = true) 15 | public class Client { 16 | private String clientId; 17 | private String resourceIds; 18 | private Boolean secretRequire; 19 | private String clientSecret; 20 | private Boolean scopeRequire; 21 | private String scope; 22 | private String authorizedGrantTypes; 23 | private String webServerRedirectUri; 24 | private String authorities; 25 | private Integer accessTokenValidity; 26 | private Integer refreshTokenValidity; 27 | } 28 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/domain/entity/User.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.domain.entity; 2 | 3 | import java.util.List; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.NoArgsConstructor; 10 | 11 | /** 12 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 13 | */ 14 | @Data 15 | @EqualsAndHashCode(callSuper = false) 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | @Builder(toBuilder = true) 19 | public class User { 20 | private Long id; 21 | private String username; 22 | private String password; 23 | private Integer status; 24 | private List roles; 25 | } 26 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/exception/Oauth2ExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.exception; 2 | 3 | import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; 4 | import org.springframework.web.bind.annotation.ControllerAdvice; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | import org.springframework.web.bind.annotation.ResponseBody; 7 | 8 | import cn.gathub.auth.api.CommonResult; 9 | 10 | 11 | /** 12 | * 全局处理Oauth2抛出的异常 13 | * 14 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 15 | */ 16 | @ControllerAdvice 17 | public class Oauth2ExceptionHandler { 18 | @ResponseBody 19 | @ExceptionHandler(value = OAuth2Exception.class) 20 | public CommonResult handleOauth2(OAuth2Exception e) { 21 | return CommonResult.failed(e.getMessage()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/service/ClientService.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.service; 2 | 3 | import org.springframework.security.oauth2.provider.ClientDetailsService; 4 | 5 | /** 6 | * @author Honghui [wanghonghui_work@163.com] 2021/3/18 7 | */ 8 | public interface ClientService extends ClientDetailsService { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/service/ResourceService.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.service; 2 | 3 | /** 4 | * @author Honghui [wanghonghui_work@163.com] 2021/3/18 5 | */ 6 | public interface ResourceService { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/service/UserService.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.service; 2 | 3 | import org.springframework.security.core.userdetails.UserDetailsService; 4 | 5 | /** 6 | * @author Honghui [wanghonghui_work@163.com] 2021/3/18 7 | */ 8 | public interface UserService extends UserDetailsService { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/service/impl/ClientServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.service.impl; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.security.crypto.password.PasswordEncoder; 5 | import org.springframework.security.oauth2.provider.ClientDetails; 6 | import org.springframework.security.oauth2.provider.ClientRegistrationException; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.server.ResponseStatusException; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | import javax.annotation.PostConstruct; 15 | 16 | import cn.gathub.auth.constant.MessageConstant; 17 | import cn.gathub.auth.domain.entity.Client; 18 | import cn.gathub.auth.service.ClientService; 19 | import cn.gathub.auth.service.principal.ClientPrincipal; 20 | import cn.hutool.core.collection.CollUtil; 21 | 22 | /** 23 | * 客户端管理业务类 24 | * 25 | * @author Honghui [wanghonghui_work@163.com] 2021/3/18 26 | */ 27 | @Service 28 | public class ClientServiceImpl implements ClientService { 29 | 30 | private List clientList; 31 | private final PasswordEncoder passwordEncoder; 32 | 33 | public ClientServiceImpl(PasswordEncoder passwordEncoder) { 34 | this.passwordEncoder = passwordEncoder; 35 | } 36 | 37 | @PostConstruct 38 | public void initData() { 39 | String clientSecret = passwordEncoder.encode("123456"); 40 | clientList = new ArrayList<>(); 41 | // 1、密码模式 42 | clientList.add(Client.builder() 43 | .clientId("client-app") 44 | .resourceIds("oauth2-resource") 45 | .secretRequire(false) 46 | .clientSecret(clientSecret) 47 | .scopeRequire(false) 48 | .scope("all") 49 | .authorizedGrantTypes("password,refresh_token") 50 | .authorities("ADMIN,USER") 51 | .accessTokenValidity(3600) 52 | .refreshTokenValidity(86400).build()); 53 | // 2、授权码模式 54 | clientList.add(Client.builder() 55 | .clientId("client-app-2") 56 | .resourceIds("oauth2-resource2") 57 | .secretRequire(false) 58 | .clientSecret(clientSecret) 59 | .scopeRequire(false) 60 | .scope("all") 61 | .authorizedGrantTypes("authorization_code,refresh_token") 62 | .webServerRedirectUri("https://www.gathub.cn,https://www.baidu.com") 63 | .authorities("USER") 64 | .accessTokenValidity(3600) 65 | .refreshTokenValidity(86400).build()); 66 | } 67 | 68 | @Override 69 | public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException { 70 | List findClientList = clientList.stream().filter(item -> item.getClientId().equals(clientId)).collect(Collectors.toList()); 71 | if (CollUtil.isEmpty(findClientList)) { 72 | throw new ResponseStatusException(HttpStatus.NOT_FOUND, MessageConstant.NOT_FOUND_CLIENT); 73 | } 74 | return new ClientPrincipal(findClientList.get(0)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/service/impl/ResourceServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.service.impl; 2 | 3 | import org.springframework.data.redis.core.RedisTemplate; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.TreeMap; 9 | 10 | import javax.annotation.PostConstruct; 11 | 12 | import cn.gathub.auth.constant.RedisConstant; 13 | import cn.gathub.auth.service.ResourceService; 14 | import cn.hutool.core.collection.CollUtil; 15 | 16 | /** 17 | * 资源与角色匹配关系管理业务类 18 | * 19 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 20 | */ 21 | @Service 22 | public class ResourceServiceImpl implements ResourceService { 23 | 24 | private final RedisTemplate redisTemplate; 25 | 26 | public ResourceServiceImpl(RedisTemplate redisTemplate) { 27 | this.redisTemplate = redisTemplate; 28 | } 29 | 30 | @PostConstruct 31 | public void initData() { 32 | Map> resourceRolesMap = new TreeMap<>(); 33 | resourceRolesMap.put("/resource/hello", CollUtil.toList("ADMIN")); 34 | resourceRolesMap.put("/resource/user/currentUser", CollUtil.toList("ADMIN", "USER")); 35 | redisTemplate.opsForHash().putAll(RedisConstant.RESOURCE_ROLES_MAP, resourceRolesMap); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.service.impl; 2 | 3 | import org.springframework.security.authentication.AccountExpiredException; 4 | import org.springframework.security.authentication.CredentialsExpiredException; 5 | import org.springframework.security.authentication.DisabledException; 6 | import org.springframework.security.authentication.LockedException; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 9 | import org.springframework.security.crypto.password.PasswordEncoder; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | import javax.annotation.PostConstruct; 17 | 18 | import cn.gathub.auth.constant.MessageConstant; 19 | import cn.gathub.auth.domain.entity.User; 20 | import cn.gathub.auth.service.UserService; 21 | import cn.gathub.auth.service.principal.UserPrincipal; 22 | import cn.hutool.core.collection.CollUtil; 23 | 24 | /** 25 | * 用户管理业务类 26 | * 27 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 28 | */ 29 | @Service 30 | public class UserServiceImpl implements UserService { 31 | 32 | private List userList; 33 | private final PasswordEncoder passwordEncoder; 34 | 35 | public UserServiceImpl(PasswordEncoder passwordEncoder) { 36 | this.passwordEncoder = passwordEncoder; 37 | } 38 | 39 | @PostConstruct 40 | public void initData() { 41 | String password = passwordEncoder.encode("123456"); 42 | userList = new ArrayList<>(); 43 | userList.add(new User(1L, "admin", password, 1, CollUtil.toList("ADMIN"))); 44 | userList.add(new User(2L, "user", password, 1, CollUtil.toList("USER"))); 45 | } 46 | 47 | @Override 48 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 49 | List findUserList = userList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList()); 50 | if (CollUtil.isEmpty(findUserList)) { 51 | throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); 52 | } 53 | UserPrincipal userPrincipal = new UserPrincipal(findUserList.get(0)); 54 | if (!userPrincipal.isEnabled()) { 55 | throw new DisabledException(MessageConstant.ACCOUNT_DISABLED); 56 | } else if (!userPrincipal.isAccountNonLocked()) { 57 | throw new LockedException(MessageConstant.ACCOUNT_LOCKED); 58 | } else if (!userPrincipal.isAccountNonExpired()) { 59 | throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED); 60 | } else if (!userPrincipal.isCredentialsNonExpired()) { 61 | throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED); 62 | } 63 | return userPrincipal; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/service/principal/ClientPrincipal.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.service.principal; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.oauth2.provider.ClientDetails; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.Collection; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | import java.util.Set; 12 | 13 | import cn.gathub.auth.constant.MessageConstant; 14 | import cn.gathub.auth.domain.entity.Client; 15 | import lombok.Data; 16 | 17 | /** 18 | * 登录用户信息 19 | * 20 | * @author Honghui [wanghonghui_work@163.com] 2021/3/19 21 | */ 22 | @Data 23 | public class ClientPrincipal implements ClientDetails { 24 | 25 | private Client client; 26 | 27 | public ClientPrincipal(Client client) { 28 | this.client = client; 29 | } 30 | 31 | @Override 32 | public String getClientId() { 33 | return client.getClientId(); 34 | } 35 | 36 | @Override 37 | public Set getResourceIds() { 38 | return new HashSet<>(Arrays.asList(client.getResourceIds().split(MessageConstant.SPLIT_COMMA))); 39 | } 40 | 41 | @Override 42 | public boolean isSecretRequired() { 43 | return client.getSecretRequire(); 44 | } 45 | 46 | @Override 47 | public String getClientSecret() { 48 | return client.getClientSecret(); 49 | } 50 | 51 | @Override 52 | public boolean isScoped() { 53 | return client.getScopeRequire(); 54 | } 55 | 56 | @Override 57 | public Set getScope() { 58 | return new HashSet<>(Arrays.asList(client.getScope().split(MessageConstant.SPLIT_COMMA))); 59 | } 60 | 61 | @Override 62 | public Set getAuthorizedGrantTypes() { 63 | return new HashSet<>(Arrays.asList(client.getAuthorizedGrantTypes().split(MessageConstant.SPLIT_COMMA))); 64 | } 65 | 66 | @Override 67 | public Set getRegisteredRedirectUri() { 68 | return new HashSet<>(Arrays.asList(client.getWebServerRedirectUri().split(MessageConstant.SPLIT_COMMA))); 69 | } 70 | 71 | @Override 72 | public Collection getAuthorities() { 73 | Collection collection = new ArrayList<>(); 74 | Arrays.asList(client.getAuthorities().split(MessageConstant.SPLIT_COMMA)).forEach( 75 | auth -> collection.add((GrantedAuthority) () -> auth) 76 | ); 77 | return collection; 78 | } 79 | 80 | @Override 81 | public Integer getAccessTokenValiditySeconds() { 82 | return client.getAccessTokenValidity(); 83 | } 84 | 85 | @Override 86 | public Integer getRefreshTokenValiditySeconds() { 87 | return client.getRefreshTokenValidity(); 88 | } 89 | 90 | @Override 91 | public boolean isAutoApprove(String s) { 92 | return false; 93 | } 94 | 95 | @Override 96 | public Map getAdditionalInformation() { 97 | return null; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/java/cn/gathub/auth/service/principal/UserPrincipal.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.auth.service.principal; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | 10 | import cn.gathub.auth.domain.entity.User; 11 | import lombok.Data; 12 | 13 | /** 14 | * 登录用户信息 15 | * 16 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 17 | */ 18 | @Data 19 | public class UserPrincipal implements UserDetails { 20 | 21 | /** 22 | * ID 23 | */ 24 | private Long id; 25 | /** 26 | * 用户名 27 | */ 28 | private String username; 29 | /** 30 | * 用户密码 31 | */ 32 | private String password; 33 | /** 34 | * 用户状态 35 | */ 36 | private Boolean enabled; 37 | /** 38 | * 权限数据 39 | */ 40 | private Collection authorities; 41 | 42 | public UserPrincipal(User user) { 43 | this.setId(user.getId()); 44 | this.setUsername(user.getUsername()); 45 | this.setPassword(user.getPassword()); 46 | this.setEnabled(user.getStatus() == 1); 47 | if (user.getRoles() != null) { 48 | authorities = new ArrayList<>(); 49 | user.getRoles().forEach(item -> authorities.add(new SimpleGrantedAuthority(item))); 50 | } 51 | } 52 | 53 | @Override 54 | public Collection getAuthorities() { 55 | return this.authorities; 56 | } 57 | 58 | @Override 59 | public String getPassword() { 60 | return this.password; 61 | } 62 | 63 | @Override 64 | public String getUsername() { 65 | return this.username; 66 | } 67 | 68 | @Override 69 | public boolean isAccountNonExpired() { 70 | return true; 71 | } 72 | 73 | @Override 74 | public boolean isAccountNonLocked() { 75 | return true; 76 | } 77 | 78 | @Override 79 | public boolean isCredentialsNonExpired() { 80 | return true; 81 | } 82 | 83 | @Override 84 | public boolean isEnabled() { 85 | return this.enabled; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9401 3 | spring: 4 | profiles: 5 | active: dev 6 | application: 7 | name: oauth2-auth 8 | cloud: 9 | nacos: 10 | discovery: 11 | server-addr: localhost:8848 12 | jackson: 13 | date-format: yyyy-MM-dd HH:mm:ss 14 | redis: 15 | database: 0 16 | port: 6379 17 | host: localhost 18 | password: 19 | management: 20 | endpoints: 21 | web: 22 | exposure: 23 | include: "*" 24 | -------------------------------------------------------------------------------- /oauth2-auth/src/main/resources/jwt.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-honghui/spring-cloud-gateway-oauth2/1afb5590df8b3726a22036ef39fbf7c87ac11fb6/oauth2-auth/src/main/resources/jwt.jks -------------------------------------------------------------------------------- /oauth2-gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | gathub-oauth2 7 | cn.gathub 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | oauth2-gateway 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-webflux 18 | 19 | 20 | org.springframework.cloud 21 | spring-cloud-starter-gateway 22 | 23 | 24 | org.springframework.security 25 | spring-security-config 26 | 27 | 28 | org.springframework.security 29 | spring-security-oauth2-resource-server 30 | 31 | 32 | org.springframework.security 33 | spring-security-oauth2-client 34 | 35 | 36 | org.springframework.security 37 | spring-security-oauth2-jose 38 | 39 | 40 | com.nimbusds 41 | nimbus-jose-jwt 42 | 8.16 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-data-redis 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-configuration-processor 53 | true 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/Oauth2GatewayApplication.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | /** 8 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 9 | */ 10 | @EnableDiscoveryClient 11 | @SpringBootApplication 12 | public class Oauth2GatewayApplication { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(Oauth2GatewayApplication.class, args); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/api/CommonResult.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.api; 2 | 3 | /** 4 | * 通用返回对象 5 | * 6 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 7 | */ 8 | public class CommonResult { 9 | private long code; 10 | private String message; 11 | private T data; 12 | 13 | protected CommonResult() { 14 | } 15 | 16 | protected CommonResult(long code, String message, T data) { 17 | this.code = code; 18 | this.message = message; 19 | this.data = data; 20 | } 21 | 22 | /** 23 | * 成功返回结果 24 | * 25 | * @param data 获取的数据 26 | */ 27 | public static CommonResult success(T data) { 28 | return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); 29 | } 30 | 31 | /** 32 | * 成功返回结果 33 | * 34 | * @param data 获取的数据 35 | * @param message 提示信息 36 | */ 37 | public static CommonResult success(T data, String message) { 38 | return new CommonResult(ResultCode.SUCCESS.getCode(), message, data); 39 | } 40 | 41 | /** 42 | * 失败返回结果 43 | * 44 | * @param errorCode 错误码 45 | */ 46 | public static CommonResult failed(IErrorCode errorCode) { 47 | return new CommonResult(errorCode.getCode(), errorCode.getMessage(), null); 48 | } 49 | 50 | /** 51 | * 失败返回结果 52 | * 53 | * @param errorCode 错误码 54 | * @param message 错误信息 55 | */ 56 | public static CommonResult failed(IErrorCode errorCode, String message) { 57 | return new CommonResult(errorCode.getCode(), message, null); 58 | } 59 | 60 | /** 61 | * 失败返回结果 62 | * 63 | * @param message 提示信息 64 | */ 65 | public static CommonResult failed(String message) { 66 | return new CommonResult(ResultCode.FAILED.getCode(), message, null); 67 | } 68 | 69 | /** 70 | * 失败返回结果 71 | */ 72 | public static CommonResult failed() { 73 | return failed(ResultCode.FAILED); 74 | } 75 | 76 | /** 77 | * 参数验证失败返回结果 78 | */ 79 | public static CommonResult validateFailed() { 80 | return failed(ResultCode.VALIDATE_FAILED); 81 | } 82 | 83 | /** 84 | * 参数验证失败返回结果 85 | * 86 | * @param message 提示信息 87 | */ 88 | public static CommonResult validateFailed(String message) { 89 | return new CommonResult(ResultCode.VALIDATE_FAILED.getCode(), message, null); 90 | } 91 | 92 | /** 93 | * 未登录返回结果 94 | */ 95 | public static CommonResult unauthorized(T data) { 96 | return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); 97 | } 98 | 99 | /** 100 | * 未授权返回结果 101 | */ 102 | public static CommonResult forbidden(T data) { 103 | return new CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); 104 | } 105 | 106 | public long getCode() { 107 | return code; 108 | } 109 | 110 | public void setCode(long code) { 111 | this.code = code; 112 | } 113 | 114 | public String getMessage() { 115 | return message; 116 | } 117 | 118 | public void setMessage(String message) { 119 | this.message = message; 120 | } 121 | 122 | public T getData() { 123 | return data; 124 | } 125 | 126 | public void setData(T data) { 127 | this.data = data; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/api/IErrorCode.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.api; 2 | 3 | /** 4 | * 封装API的错误码 5 | * 6 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 7 | */ 8 | public interface IErrorCode { 9 | long getCode(); 10 | 11 | String getMessage(); 12 | } 13 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/api/ResultCode.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.api; 2 | 3 | /** 4 | * 枚举了一些常用API操作码 5 | * 6 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 7 | */ 8 | public enum ResultCode implements IErrorCode { 9 | SUCCESS(200, "操作成功"), 10 | FAILED(500, "操作失败"), 11 | VALIDATE_FAILED(404, "参数检验失败"), 12 | UNAUTHORIZED(401, "暂未登录或token已经过期"), 13 | FORBIDDEN(403, "没有相关权限"); 14 | private final long code; 15 | private final String message; 16 | 17 | ResultCode(long code, String message) { 18 | this.code = code; 19 | this.message = message; 20 | } 21 | 22 | public long getCode() { 23 | return code; 24 | } 25 | 26 | public String getMessage() { 27 | return message; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/authorization/AuthorizationManager.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.authorization; 2 | 3 | 4 | import org.springframework.data.redis.core.RedisTemplate; 5 | import org.springframework.security.authorization.AuthorizationDecision; 6 | import org.springframework.security.authorization.ReactiveAuthorizationManager; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.GrantedAuthority; 9 | import org.springframework.security.web.server.authorization.AuthorizationContext; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.net.URI; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | import cn.gathub.gateway.constant.AuthConstant; 17 | import cn.gathub.gateway.constant.RedisConstant; 18 | import cn.hutool.core.convert.Convert; 19 | import reactor.core.publisher.Mono; 20 | 21 | /** 22 | * 鉴权管理器,用于判断是否有资源的访问权限 23 | * 24 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 25 | */ 26 | @Component 27 | public class AuthorizationManager implements ReactiveAuthorizationManager { 28 | private final RedisTemplate redisTemplate; 29 | 30 | public AuthorizationManager(RedisTemplate redisTemplate) { 31 | this.redisTemplate = redisTemplate; 32 | } 33 | 34 | @Override 35 | public Mono check(Mono mono, AuthorizationContext authorizationContext) { 36 | // 1、从Redis中获取当前路径可访问角色列表 37 | URI uri = authorizationContext.getExchange().getRequest().getURI(); 38 | Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath()); 39 | List authorities = Convert.toList(String.class, obj); 40 | authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList()); 41 | // 2、认证通过且角色匹配的用户可访问当前路径 42 | return mono 43 | .filter(Authentication::isAuthenticated) 44 | .flatMapIterable(Authentication::getAuthorities) 45 | .map(GrantedAuthority::getAuthority) 46 | .any(authorities::contains) 47 | .map(AuthorizationDecision::new) 48 | .defaultIfEmpty(new AuthorizationDecision(false)); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/component/RestAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.component; 2 | 3 | import org.springframework.core.io.buffer.DataBuffer; 4 | import org.springframework.http.HttpHeaders; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.http.server.reactive.ServerHttpResponse; 8 | import org.springframework.security.core.AuthenticationException; 9 | import org.springframework.security.web.server.ServerAuthenticationEntryPoint; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.web.server.ServerWebExchange; 12 | 13 | import java.nio.charset.StandardCharsets; 14 | 15 | import cn.gathub.gateway.api.CommonResult; 16 | import cn.hutool.json.JSONUtil; 17 | import reactor.core.publisher.Mono; 18 | 19 | /** 20 | * 自定义返回结果:没有登录或token过期时 21 | * 22 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 23 | */ 24 | @Component 25 | public class RestAuthenticationEntryPoint implements ServerAuthenticationEntryPoint { 26 | @Override 27 | public Mono commence(ServerWebExchange exchange, AuthenticationException e) { 28 | ServerHttpResponse response = exchange.getResponse(); 29 | response.setStatusCode(HttpStatus.UNAUTHORIZED); 30 | response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); 31 | String body = JSONUtil.toJsonStr(CommonResult.unauthorized(e.getMessage())); 32 | DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); 33 | return response.writeWith(Mono.just(buffer)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/component/RestfulAccessDeniedHandler.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.component; 2 | 3 | import org.springframework.core.io.buffer.DataBuffer; 4 | import org.springframework.http.HttpHeaders; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.http.server.reactive.ServerHttpResponse; 8 | import org.springframework.security.access.AccessDeniedException; 9 | import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.web.server.ServerWebExchange; 12 | 13 | import java.nio.charset.StandardCharsets; 14 | 15 | import cn.gathub.gateway.api.CommonResult; 16 | import cn.hutool.json.JSONUtil; 17 | import reactor.core.publisher.Mono; 18 | 19 | /** 20 | * 自定义返回结果:没有权限访问时 21 | * 22 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 23 | */ 24 | @Component 25 | public class RestfulAccessDeniedHandler implements ServerAccessDeniedHandler { 26 | @Override 27 | public Mono handle(ServerWebExchange exchange, AccessDeniedException denied) { 28 | ServerHttpResponse response = exchange.getResponse(); 29 | response.setStatusCode(HttpStatus.FORBIDDEN); 30 | response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); 31 | String body = JSONUtil.toJsonStr(CommonResult.forbidden(denied.getMessage())); 32 | DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); 33 | return response.writeWith(Mono.just(buffer)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/config/IgnoreUrlsConfig.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.util.List; 7 | 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | 11 | /** 12 | * 网关白名单配置 13 | * 14 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 15 | */ 16 | @Data 17 | @EqualsAndHashCode(callSuper = false) 18 | @Component 19 | @ConfigurationProperties(prefix = "secure.ignore") 20 | public class IgnoreUrlsConfig { 21 | private List urls; 22 | } 23 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/config/RedisRepositoryConfig.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.redis.connection.RedisConnectionFactory; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; 8 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 9 | import org.springframework.data.redis.serializer.StringRedisSerializer; 10 | 11 | /** 12 | * Redis相关配置 13 | * 14 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 15 | */ 16 | @Configuration 17 | @EnableRedisRepositories 18 | public class RedisRepositoryConfig { 19 | 20 | @Bean 21 | public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { 22 | RedisTemplate redisTemplate = new RedisTemplate<>(); 23 | redisTemplate.setConnectionFactory(connectionFactory); 24 | StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); 25 | redisTemplate.setKeySerializer(stringRedisSerializer); 26 | redisTemplate.setHashKeySerializer(stringRedisSerializer); 27 | Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); 28 | redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); 29 | redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); 30 | redisTemplate.afterPropertiesSet(); 31 | return redisTemplate; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/config/ResourceServerConfig.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.config; 2 | 3 | 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.core.convert.converter.Converter; 7 | import org.springframework.security.authentication.AbstractAuthenticationToken; 8 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 9 | import org.springframework.security.config.web.server.SecurityWebFiltersOrder; 10 | import org.springframework.security.config.web.server.ServerHttpSecurity; 11 | import org.springframework.security.oauth2.jwt.Jwt; 12 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; 13 | import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; 14 | import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; 15 | import org.springframework.security.web.server.SecurityWebFilterChain; 16 | 17 | import cn.gathub.gateway.authorization.AuthorizationManager; 18 | import cn.gathub.gateway.component.RestAuthenticationEntryPoint; 19 | import cn.gathub.gateway.component.RestfulAccessDeniedHandler; 20 | import cn.gathub.gateway.constant.AuthConstant; 21 | import cn.gathub.gateway.filter.IgnoreUrlsRemoveJwtFilter; 22 | import cn.hutool.core.util.ArrayUtil; 23 | import lombok.AllArgsConstructor; 24 | import reactor.core.publisher.Mono; 25 | 26 | /** 27 | * 资源服务器配置 28 | * 29 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 30 | */ 31 | @AllArgsConstructor 32 | @Configuration 33 | @EnableWebFluxSecurity 34 | public class ResourceServerConfig { 35 | private final AuthorizationManager authorizationManager; 36 | private final IgnoreUrlsConfig ignoreUrlsConfig; 37 | private final RestfulAccessDeniedHandler restfulAccessDeniedHandler; 38 | private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; 39 | private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter; 40 | 41 | @Bean 42 | public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { 43 | http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter()); 44 | // 1、自定义处理JWT请求头过期或签名错误的结果 45 | http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint); 46 | // 2、对白名单路径,直接移除JWT请求头 47 | http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION); 48 | http.authorizeExchange() 49 | .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(), String.class)).permitAll() // 白名单配置 50 | .anyExchange().access(authorizationManager) // 鉴权管理器配置 51 | .and().exceptionHandling() 52 | .accessDeniedHandler(restfulAccessDeniedHandler) // 处理未授权 53 | .authenticationEntryPoint(restAuthenticationEntryPoint) // 处理未认证 54 | .and().csrf().disable(); 55 | return http.build(); 56 | } 57 | 58 | @Bean 59 | public Converter> jwtAuthenticationConverter() { 60 | JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); 61 | jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX); 62 | jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME); 63 | JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); 64 | jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); 65 | return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/constant/AuthConstant.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.constant; 2 | 3 | /** 4 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 5 | */ 6 | public class AuthConstant { 7 | 8 | public static final String AUTHORITY_PREFIX = "ROLE_"; 9 | public static final String AUTHORITY_CLAIM_NAME = "authorities"; 10 | } 11 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/constant/RedisConstant.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.constant; 2 | 3 | /** 4 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 5 | */ 6 | public class RedisConstant { 7 | 8 | public static final String RESOURCE_ROLES_MAP = "AUTH:RESOURCE_ROLES_MAP"; 9 | } 10 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/filter/AuthGlobalFilter.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.filter; 2 | 3 | import com.nimbusds.jose.JWSObject; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 8 | import org.springframework.cloud.gateway.filter.GlobalFilter; 9 | import org.springframework.core.Ordered; 10 | import org.springframework.http.server.reactive.ServerHttpRequest; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.web.server.ServerWebExchange; 13 | 14 | import java.text.ParseException; 15 | 16 | import cn.hutool.core.util.StrUtil; 17 | import reactor.core.publisher.Mono; 18 | 19 | /** 20 | * 将登录用户的JWT转化成用户信息的全局过滤器 21 | * 22 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 23 | */ 24 | @Component 25 | public class AuthGlobalFilter implements GlobalFilter, Ordered { 26 | 27 | private final static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class); 28 | 29 | @Override 30 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { 31 | String token = exchange.getRequest().getHeaders().getFirst("Authorization"); 32 | if (StrUtil.isEmpty(token)) { 33 | return chain.filter(exchange); 34 | } 35 | try { 36 | // 从token中解析用户信息并设置到Header中去 37 | String realToken = token.replace("Bearer ", ""); 38 | JWSObject jwsObject = JWSObject.parse(realToken); 39 | String userStr = jwsObject.getPayload().toString(); 40 | LOGGER.info("AuthGlobalFilter.filter() user:{}", userStr); 41 | ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build(); 42 | exchange = exchange.mutate().request(request).build(); 43 | } catch (ParseException e) { 44 | e.printStackTrace(); 45 | } 46 | return chain.filter(exchange); 47 | } 48 | 49 | @Override 50 | public int getOrder() { 51 | return 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/java/cn/gathub/gateway/filter/IgnoreUrlsRemoveJwtFilter.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.gateway.filter; 2 | 3 | import org.springframework.http.server.reactive.ServerHttpRequest; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.util.AntPathMatcher; 6 | import org.springframework.util.PathMatcher; 7 | import org.springframework.web.server.ServerWebExchange; 8 | import org.springframework.web.server.WebFilter; 9 | import org.springframework.web.server.WebFilterChain; 10 | 11 | import java.net.URI; 12 | import java.util.List; 13 | 14 | import cn.gathub.gateway.config.IgnoreUrlsConfig; 15 | import reactor.core.publisher.Mono; 16 | 17 | /** 18 | * 白名单路径访问时需要移除JWT请求头 19 | * 20 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 21 | */ 22 | @Component 23 | public class IgnoreUrlsRemoveJwtFilter implements WebFilter { 24 | 25 | private final IgnoreUrlsConfig ignoreUrlsConfig; 26 | 27 | public IgnoreUrlsRemoveJwtFilter(IgnoreUrlsConfig ignoreUrlsConfig) { 28 | this.ignoreUrlsConfig = ignoreUrlsConfig; 29 | } 30 | 31 | @Override 32 | public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { 33 | ServerHttpRequest request = exchange.getRequest(); 34 | URI uri = request.getURI(); 35 | PathMatcher pathMatcher = new AntPathMatcher(); 36 | // 白名单路径移除JWT请求头 37 | List ignoreUrls = ignoreUrlsConfig.getUrls(); 38 | for (String ignoreUrl : ignoreUrls) { 39 | if (pathMatcher.match(ignoreUrl, uri.getPath())) { 40 | request = exchange.getRequest().mutate().header("Authorization", "").build(); 41 | exchange = exchange.mutate().request(request).build(); 42 | return chain.filter(exchange); 43 | } 44 | } 45 | return chain.filter(exchange); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /oauth2-gateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9201 3 | spring: 4 | profiles: 5 | active: dev 6 | application: 7 | name: oauth2-gateway 8 | cloud: 9 | nacos: 10 | discovery: 11 | server-addr: localhost:8848 12 | gateway: 13 | routes: # 配置路由路径 14 | - id: oauth2-resource-route 15 | uri: lb://oauth2-resource 16 | predicates: 17 | - Path=/resource/** 18 | filters: 19 | - StripPrefix=1 20 | - id: oauth2-auth-route 21 | uri: lb://oauth2-auth 22 | predicates: 23 | - Path=/auth/** 24 | filters: 25 | - StripPrefix=1 26 | - id: oauth2-auth-login 27 | uri: lb://oauth2-auth 28 | predicates: 29 | - Path=/login 30 | filters: 31 | - PreserveHostHeader 32 | - id: oauth2-auth-token 33 | uri: lb://oauth2-auth 34 | predicates: 35 | - Path=/oauth/token 36 | filters: 37 | - PreserveHostHeader 38 | - id: oauth2-auth-authorize 39 | uri: lb://oauth2-auth 40 | predicates: 41 | - Path=/oauth/authorize 42 | filters: 43 | - PreserveHostHeader 44 | discovery: 45 | locator: 46 | enabled: true # 开启从注册中心动态创建路由的功能 47 | lower-case-service-id: true # 使用小写服务名,默认是大写 48 | security: 49 | oauth2: 50 | resourceserver: 51 | jwt: 52 | jwk-set-uri: 'http://localhost:9401/rsa/publicKey' # 配置RSA的公钥访问地址 53 | redis: 54 | database: 0 55 | port: 6379 56 | host: localhost 57 | password: 58 | secure: 59 | ignore: 60 | urls: # 配置白名单路径 61 | - "/actuator/**" 62 | - "/oauth/token" 63 | - "/oauth/authorize" 64 | - "/login" 65 | -------------------------------------------------------------------------------- /oauth2-resource/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | gathub-oauth2 7 | cn.gathub 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | oauth2-resource 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-web 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /oauth2-resource/src/main/java/cn/gathub/resource/Oauth2ResourceApplication.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.resource; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | /** 8 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 9 | */ 10 | @EnableDiscoveryClient 11 | @SpringBootApplication 12 | public class Oauth2ResourceApplication { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(Oauth2ResourceApplication.class, args); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /oauth2-resource/src/main/java/cn/gathub/resource/controller/HelloController.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.resource.controller; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | /** 7 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 8 | */ 9 | @RestController 10 | public class HelloController { 11 | 12 | @GetMapping("/hello") 13 | public String hello() { 14 | return "Hello World !"; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /oauth2-resource/src/main/java/cn/gathub/resource/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.resource.controller; 2 | 3 | 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import javax.servlet.http.HttpServletRequest; 9 | 10 | import cn.gathub.resource.domain.User; 11 | import cn.hutool.core.convert.Convert; 12 | import cn.hutool.core.lang.UUID; 13 | import cn.hutool.json.JSONObject; 14 | 15 | /** 16 | * 获取登录用户信息接口 17 | * 18 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 19 | */ 20 | @RestController 21 | @RequestMapping("/user") 22 | public class UserController { 23 | 24 | @GetMapping("/currentUser") 25 | public User currentUser(HttpServletRequest request) { 26 | // 从Header中获取用户信息 27 | String userStr = request.getHeader("user"); 28 | JSONObject userJsonObject = new JSONObject(userStr); 29 | return User.builder() 30 | .username(userJsonObject.getStr("user_name")) 31 | .id(Convert.toLong(userJsonObject.get("id"))) 32 | .roles(Convert.toList(String.class, userJsonObject.get("authorities"))).build(); 33 | } 34 | 35 | @GetMapping 36 | public JSONObject findUser(HttpServletRequest request) { 37 | // 从Header中获取用户信息 38 | String userStr = request.getHeader("user"); 39 | return new JSONObject(userStr); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /oauth2-resource/src/main/java/cn/gathub/resource/domain/User.java: -------------------------------------------------------------------------------- 1 | package cn.gathub.resource.domain; 2 | 3 | import java.util.List; 4 | 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | 9 | /** 10 | * @author Honghui [wanghonghui_work@163.com] 2021/3/16 11 | */ 12 | @Data 13 | @EqualsAndHashCode(callSuper = false) 14 | @Builder(toBuilder = true) 15 | public class User { 16 | private Long id; 17 | private String username; 18 | private String password; 19 | private List roles; 20 | } 21 | -------------------------------------------------------------------------------- /oauth2-resource/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9501 3 | spring: 4 | profiles: 5 | active: dev 6 | application: 7 | name: oauth2-resource 8 | cloud: 9 | nacos: 10 | discovery: 11 | server-addr: localhost:8848 12 | management: 13 | endpoints: 14 | web: 15 | exposure: 16 | include: "*" 17 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.gathub 8 | gathub-oauth2 9 | pom 10 | 1.0.0 11 | 12 | 13 | oauth2-gateway 14 | oauth2-auth 15 | oauth2-resource 16 | 17 | 18 | 19 | UTF-8 20 | 1.8 21 | 1.8 22 | 1.8 23 | 2.2.0.RELEASE 24 | Hoxton.SR5 25 | 2.3.0.RELEASE 26 | 5.3.5 27 | 28 | 29 | 30 | 31 | 32 | com.alibaba.cloud 33 | spring-cloud-alibaba-dependencies 34 | ${spring-cloud-alibaba.version} 35 | pom 36 | import 37 | 38 | 39 | org.springframework.cloud 40 | spring-cloud-dependencies 41 | ${spring-cloud.version} 42 | pom 43 | import 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-dependencies 48 | ${spring-boot.version} 49 | pom 50 | import 51 | 52 | 53 | 54 | 55 | 56 | 57 | com.alibaba.cloud 58 | spring-cloud-starter-alibaba-nacos-discovery 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-starter-actuator 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-test 67 | test 68 | 69 | 70 | org.junit.vintage 71 | junit-vintage-engine 72 | 73 | 74 | 75 | 76 | org.projectlombok 77 | lombok 78 | true 79 | 80 | 81 | cn.hutool 82 | hutool-all 83 | ${hutool-version} 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.springframework.boot 91 | spring-boot-maven-plugin 92 | 93 | 94 | 95 | 96 | 97 | 98 | spring-milestones 99 | Spring Milestones 100 | https://repo.spring.io/milestone 101 | 102 | 103 | 104 | 105 | --------------------------------------------------------------------------------