一、环境生态 Java17+, springcloud 2024.0.1, spring-boot3.4.6, OAuth2 6.4.x
二、公共基础依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.6</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2024.0.1</version> <scope>import</scope> <type>pom</type> </dependency> </dependencies> </dependencyManagement>
三、认证服务器搭建 1. POM依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <dependencies> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.12</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser</artifactId> <version>3.5.12</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>1.78.1</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk18on</artifactId> <version>1.78.1</version> </dependency> </dependencies>
2. 代码配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 @Configuration @Order(Integer.MIN_VALUE) public class AuthorizationServerConfigurer { @Value("${cus.token.jwt-privateKey}") private String privateKey; @Value("${cus.token.jwt-publicKey}") private String publicKey; @Value("${cus.token.expire:2592000}") private Integer tokenValiditySeconds; @Bean public RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) { return new JdbcRegisteredClientRepository(jdbcOperations); } @Bean public OAuth2AuthorizationService dbAuthorizationService(JdbcOperations jdbcOperations, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository); } @Bean public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() { return context -> { if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { Instant now = Instant.now(); // token附加内容,也可以加其他字段 context.getClaims().claim("copyright", "1222"); context.getClaims().issuedAt(now); context.getClaims().expiresAt(now.plusSeconds(tokenValiditySeconds)); } }; } @Bean public PasswordEncoder passwordEncoder() { //自定义你的密码加密方式 return new BCryptPasswordEncoder(); } /** * jwt配置 */ @Bean public JWKSource<SecurityContext> jwkSource() throws JOSEException { JWK jwk = RSAKey.parseFromPEMEncodedObjects(this.privateKey); JWKSet jwkSet = new JWKSet(jwk); return (jwkSelector, context) -> jwkSelector.select(jwkSet); } /** * jwt编码器 */ @Bean public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) { return new NimbusJwtEncoder(jwkSource); } /** * jwt解码器 */ @Bean public JwtDecoder jwtDecoder() throws NoSuchAlgorithmException, InvalidKeySpecException { byte[] encoded = Base64.getDecoder().decode(this.publicKey); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded); return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyFactory.generatePublic(keySpec)).build(); } @Bean @Order(1) public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http, OAuth2AuthorizationServerConfigurer configurer, EraAuthenticationEntryPoint eraAuthenticationEntryPoint, EraAccessDeniedHandler eraAccessDeniedHandler) throws Exception { return http .cors(CorsConfigurer::disable) .csrf(CsrfConfigurer::disable) .securityMatcher(PathRule.OAUTH_PATHS) .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() // 所有其他接口需要认证 ) .exceptionHandling(exception -> exception .authenticationEntryPoint(eraAuthenticationEntryPoint) .accessDeniedHandler(eraAccessDeniedHandler) ) .with(configurer, Customizer.withDefaults()) .oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults())) .build(); } //同时作为资源服务器可以加上配置 @Bean @Order(2) public SecurityFilterChain resourceSecurityFilterChain(HttpSecurity http, EraAuthenticationEntryPoint eraAuthenticationEntryPoint, EraAccessDeniedHandler eraAccessDeniedHandler) throws Exception { return http .securityMatchers(requestMatcherConfigurer -> { List<RequestMatcher> r = new ArrayList<>(); for (String oauthPath : PathRule.OAUTH_PATHS) { r.add(new AntPathRequestMatcher(oauthPath)); } //这里Negated的使用方式需要注意 requestMatcherConfigurer.requestMatchers(new NegatedRequestMatcher(new OrRequestMatcher(r))); } ) .cors(CorsConfigurer::disable) .csrf(CsrfConfigurer::disable) .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() // 所有其他接口需要认证 ) .exceptionHandling(exception -> exception .authenticationEntryPoint(eraAuthenticationEntryPoint) .accessDeniedHandler(eraAccessDeniedHandler) ) .oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults())) .build(); } //配置自定义的认证类型.这里自定义包装了一层,用于防止多次载入框架中原有的认证模式 @Bean @ConditionalOnMissingBean(OAuth2AuthorizationServerConfigurer.class) public OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer(List<EraAuthenticationConverter> eraAuthenticationConverters, List<EraAuthenticationProvider> eraAuthenticationProviders) { OAuth2AuthorizationServerConfigurer configurer = OAuth2AuthorizationServerConfigurer.authorizationServer(); configurer.tokenEndpoint(tokenEndpoint -> { if (CollectionUtil.isNotBlank(eraAuthenticationConverters)) { tokenEndpoint .accessTokenRequestConverters(converters -> { converters.addAll(eraAuthenticationConverters); }); } if (CollectionUtil.isNotBlank(eraAuthenticationProviders)) { tokenEndpoint .authenticationProviders(providers -> { providers.addAll(eraAuthenticationProviders); }); } }); return configurer; } }
1 2 public interface EraAuthenticationConverter extends AuthenticationConverter { }
1 2 public interface EraAuthenticationProvider extends AuthenticationProvider { }
1 2 3 public class PathRule { public static final String[] OAUTH_PATHS = {"/oauth/**", "/.well-known/**", "/connect/register","/oauth2/**"}; }
3. 请求接口案例 client_credentials 模式(机器到机器) 1 2 3 4 5 6 POST /oauth2/token Content-Type: application/x-www-form-urlencoded Authorization: Basic base64(client_id:client_secret) grant_type=client_credentials &scope=read
4. 自定义授权参考(短信认证模式) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Component public class SmsVerifyAuthenticationConverter implements EraAuthenticationConverter { private static final String GRANT_TYPE = "sms_verify"; private static final String PARAMETER_MOBILE = "mobile"; private static final String PARAMETER_SMSCODE = "sms_code"; @Override public Authentication convert(HttpServletRequest request) { String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); if (!GRANT_TYPE.equals(grantType)) { return null; } String phone = request.getParameter(PARAMETER_MOBILE); String smscode = request.getParameter(PARAMETER_SMSCODE); return new SmsVerifyAuthenticationToken(phone, smscode); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Component public class SmsVerifyAuthenticationProvider implements EraAuthenticationProvider { private final SmsVerifyDetailsService smsUserDetailsService; public SmsVerifyAuthenticationProvider(SmsVerifyDetailsService smsUserDetailsService) { this.smsUserDetailsService = smsUserDetailsService; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsVerifyAuthenticationToken smsCodeAuthenticationToken = (SmsVerifyAuthenticationToken) authentication; String mobile = (String) smsCodeAuthenticationToken.getPrincipal(); String smscode = (String) smsCodeAuthenticationToken.getCredentials(); UserDetails userDetails = smsUserDetailsService.loadUserByMobile(mobile, smscode); if (!userDetails.isEnabled()) { throw new UsernameNotFoundException("该手机号未注册."); } if (!userDetails.isCredentialsNonExpired()) { throw new CredentialsExpiredException("当前手机号账户已过期!"); } if (!userDetails.isAccountNonExpired()) { throw new AccountExpiredException("账户过期!"); } if (!userDetails.isAccountNonLocked()) { throw new LockedException("账户被冻结!"); } EraAuthenticationToken eraAuthenticationToken = new EraAuthenticationToken(userDetails, userDetails.getAuthorities()); eraAuthenticationToken.setDetails(authentication.getDetails()); return eraAuthenticationToken; } @Override public boolean supports(Class<?> authentication) { return SmsVerifyAuthenticationToken.class.isAssignableFrom(authentication); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class SmsVerifyAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; public SmsVerifyAuthenticationToken(Object principal) { super(principal); } public SmsVerifyAuthenticationToken(Object principal, Object credentials) { super(principal, credentials); } public SmsVerifyAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(principal, credentials, authorities); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * 具体方式自己实现 */ public interface SmsVerifyDetailsService { /** * 通过手机号查询用户 * @param mobile 手机号 * @param smsCode 验证码 * @return * @throws UsernameNotFoundException */ UserDetails loadUserByMobile(String mobile, String smsCode) throws UsernameNotFoundException; }
注意
新版的OAuth2认证自定义的认证模式无需再额外传入client_id和client_secret
四、资源服务器配置 1. POM依赖 1 2 3 4 5 6 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> </dependencies>
2. 代码配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Configuration @EnableWebSecurity public class ResourceServerConfiguration { @Bean public SecurityFilterChain securityFilterChain(PermissionWhiteListProperties permissionWhiteListProperties, HttpSecurity http) throws Exception { //白名单。自己定义即可 List<String> whites = new ArrayList<>(); http .cors(CorsConfigurer::disable) .csrf(CsrfConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers(whites.toArray(new String[whites.size()])).permitAll() .anyRequest() .authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(Customizer.withDefaults()) // 启用 JWT 解码 ); return http.build(); } //认证响应体处理。自定义即可,可以不用 @Bean public AuthenticationEntryPoint authenticationEntryPoint() { return new AuthenticationEntryPoint() { ........... }; } //认证异常响应处理。自定义即可,可以不用 @Bean public AccessDeniedHandler accessDeniedHandler() { return new AccessDeniedHandler() { ........... }; } }
3. 配置文件 1 2 3 4 5 6 spring: security: oauth2: resourceserver: jwt: jwk-set-uri: http://<资源服务器地址>/oauth2/jwks
五、客户端配置 1. POM依赖 1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency>
2. 配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 spring: security: oauth2: client: registration: <自定义客户端名称>: client-id: ylcloud client-secret: ylcloudxxdd authorization-grant-type: client_credentials provider: satellite-gateway provider: <自定义客户端名称,与上面对应>: token-uri: http://<认证服务器地址>/oauth2/token
3. 连接器配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration public class WebClientConfig { @Bean public WebClient webClient(ClientRegistrationRepository clientRepo, OAuth2AuthorizedClientRepository authorizedClientRepo) { ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRepo, authorizedClientRepo); oauth2.setDefaultClientRegistrationId("<配置文件对应的客户端名称>"); return WebClient.builder() .apply(oauth2.oauth2Configuration()) .build(); } }