OAuth2(Open Authorization 2.0)是一种用于授权的开放标准协议,用于通过第三方应用程序访问用户在某个服务提供商上存储的资源,而无需共享用户的凭证(例如用户名和密码)。它允许用户授权给第三方应用程序访问受保护的资源,同时确保用户的凭证信息不被直接暴露给第三方应用程序。
OAuth2协议的设计目标是简化授权流程和提高安全性,通过委托授权的方式和使用令牌来实现用户和第三方应用程序之间的安全通信。它已成为许多互联网服务提供商和开发者在构建应用程序时常用的授权标准。
OAuth2的作用是实现用户授权和资源访问的标准化流程,同时提供了一种安全和可扩展的方式来管理第三方应用程序访问用户资源的权限。以下是OAuth2的一些重要作用:
Spring Security OAuth2是Spring Security框架的一个扩展模块,用于实现基于OAuth2协议的身份验证和授权功能。它提供了一套易于使用和集成的API,方便开发者在Spring应用程序中实现OAuth2的各种授权模式和流程。
Spring Security OAuth2扩展了Spring Security的功能,提供了配置和管理OAuth2的客户端、授权服务器、令牌存储、权限管理等功能。它使得开发者可以轻松地构建安全的OAuth2服务和客户端应用程序。
现在,让我们深入了解OAuth2协议的流程和不同的授权模式。
在OAuth2中,首先需要进行客户端的注册和配置授权服务器。客户端是指需要访问受保护资源的应用程序,授权服务器负责验证用户身份并颁发访问令牌。
以下是一个示例代码片段,演示如何在Spring Security中进行客户端注册和授权服务器的配置:
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client1")
.secret(passwordEncoder().encode("client1secret"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("read", "write")
.redirectUris("http://localhost:8080/callback");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在上述代码中,我们使用了
@EnableAuthorizationServer
注解来启用授权服务器,并通过configure(ClientDetailsServiceConfigurer clients)
方法配置了一个内存中的客户端。客户端ID为"client1",密码为"client1secret",授权模式为"authorization_code"和"refresh_token",授权范围为"read"和"write",回调URL为"http://localhost:8080/callback"。
授权码模式是OAuth2中最常用的一种授权模式。在这种模式下,客户端通过重定向用户到授权服务器的登录页面,用户登录并同意授权后,授权服务器将授权码返回给客户端。然后,客户端使用授权码向授权服务器请求访问令牌。
以下是授权码模式的流程示例代码:
@Controller
public class AuthorizationCodeController {
@GetMapping("/authorize")
public String authorize(@RequestParam("response_type") String responseType,
@RequestParam("client_id") String clientId,
@RequestParam("redirect_uri") String redirectUri,
@RequestParam("scope") String scope,
@RequestParam("state") String state) {
// 处理授权请求,并返回授权页面
}
@GetMapping("/callback")
public String callback(@RequestParam("code") String code,
@RequestParam("state") String state) {
// 处理授权码回调,并向授权服务器请求访问令牌
}
}
在上述代码中,
/authorize
端点用于处理授权请求,通过重定向用户到授权服务器的登录页面,用户登录并同意授权后,授权服务器将授权码返回给客户端。然后,客户端会将用户重定向到redirect_uri
指定的回调URL,并在回调URL中接收授权码。/callback
端点用于处理授权码回调,客户端通过回调URL接收到授权码后,可以向授权服务器发起请求,使用授权码获取访问令牌。 类似的,还有简化模式、密码模式、客户端凭证模式和刷新令牌等授权模式。每个授权模式都有不同的流程和代码实现方式。
以下是OAuth2流程示例代码:
@Controller
public class OAuth2Controller {
@Autowired
private OAuth2RestTemplate restTemplate;
@GetMapping("/authorize")
public String authorize() {
// 重定向用户到授权服务器登录页面
}
@GetMapping("/callback")
public String callback(@RequestParam("code") String code) {
// 使用授权码向授权服务器请求访问令牌
}
@GetMapping("/protected-resource")
public String protectedResource() {
// 使用访问令牌访问受保护的资源
}
}
在上述代码中,
/authorize
端点用于重定向用户到授权服务器的登录页面。用户登录并同意授权后,授权服务器将用户重定向回客户端的回调URL,并在URL中附带授权码。/callback
端点用于处理授权码回调,客户端通过回调URL接收到授权码后,可以使用授权码向授权服务器请求访问令牌。/protected-resource
端点用于示范如何使用访问令牌访问受保护的资源。在实际应用中,你可以使用访问令牌来访问需要授权的API或资源。
3.1 OAuth2中的角色和概念:
在OAuth2协议中,有以下几个核心角色和概念:
3.2 令牌(Token)的生成和验证:
在OAuth2中,令牌是用于表示授权许可的凭证。通常,令牌由授权服务器生成,并在客户端和资源服务器之间传递和验证。
令牌的生成和验证过程可以通过以下示例代码来说明:
// 生成访问令牌的示例代码
String generateAccessToken() {
// 生成随机的访问令牌字符串
String accessToken = generateRandomToken();
// 设置访问令牌的过期时间
Date expiration = calculateExpirationDate();
// 保存访问令牌到数据库或缓存中
saveAccessToken(accessToken, expiration);
return accessToken;
}
// 验证访问令牌的示例代码
boolean validateAccessToken(String accessToken) {
// 从数据库或缓存中获取访问令牌和过期时间
AccessToken storedToken = getAccessTokenFromDatabase(accessToken);
// 检查访问令牌是否存在且未过期
if (storedToken != null && storedToken.isExpired()) {
return false;
}
return true;
}
在上述示例中,
generateAccessToken
方法用于生成随机的访问令牌,并设置其过期时间,然后将访问令牌保存到数据库或缓存中。validateAccessToken
方法用于验证传入的访问令牌是否有效,通过从数据库或缓存中获取令牌并检查其是否存在且未过期来进行验证。
在OAuth2协议中,授权服务器和资源服务器之间进行交互来验证令牌的有效性和授权许可。
以下是授权服务器和资源服务器进行交互的示例代码:
// 授权服务器验证访问令牌的示例代码
boolean validateAccessToken(String accessToken) {
// 向资源服务器发送验证请求
boolean isValid = resourceServer.validateToken(accessToken);
return isValid;
}
// 资源服务器验证访问令牌的示例代码
boolean validateToken(String accessToken) {
// 从数据库或缓存中获取访问令牌和过期时间
AccessToken storedToken = getAccessTokenFromDatabase(accessToken);
// 检查访问令牌是否存在且未过期
if (storedToken != null && storedToken.isExpired()) {
return false;
}
return true;
}
在实施OAuth2协议时,需要考虑以下安全性问题和采取相应的防护措施:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requiresChannel()
.anyRequest().requiresSecure();
}
}
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// 密钥存储在数据库或配置文件中
private static final String SECRET_KEY = "your-secret-key";
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("your-client-id")
.secret(SECRET_KEY)
.authorizedGrantTypes("password", "refresh_token")
.scopes("read", "write")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore());
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SECRET_KEY);
return converter;
}
}
资源服务器配置:
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/**").authenticated();
}
}
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// ...
@Autowired
private TokenStore tokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.tokenEnhancer(tokenEnhancerChain());
}
@Bean
public TokenEnhancerChain tokenEnhancerChain() {
TokenEnhancerChain chain = new TokenEnhancerChain();
chain.setTokenEnhancers(Arrays.asList(accessTokenConverter()));
return chain;
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/**").hasAnyRole("USER", "ADMIN")
.and()
.addFilterBefore(rateLimitFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public RateLimitFilter rateLimitFilter() {
return new RateLimitFilter();
}
}
然后建立一个Filter来进行控制 这里使用的是OncePerRequestFilter:
public class RateLimitFilter extends OncePerRequestFilter {
private static final int MAX_REQUESTS_PER_SECOND = 10;
private final Map<String, Long> requestCounts = new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String clientId = getClientId(request);
if (clientId != null) {
if (exceedsRateLimit(clientId)) {
response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "Rate limit exceeded");
return;
} else {
incrementRequestCount(clientId);
}
}
filterChain.doFilter(request, response);
}
private String getClientId(HttpServletRequest request) {
// 根据请求获取客户端ID
// 例如,从请求头中获取或从请求参数中获取
return request.getHeader("Client-Id");
}
private boolean exceedsRateLimit(String clientId) {
long currentTimestamp = System.currentTimeMillis();
requestCounts.entrySet().removeIf(entry -> entry.getValue() < currentTimestamp - 1000);
return requestCounts.compute(clientId, (k, v) -> v == null ? 1 : v + 1) > MAX_REQUESTS_PER_SECOND;
}
private void incrementRequestCount(String clientId) {
requestCounts.compute(clientId, (k, v) -> v == null ? 1 : v + 1);
}
}
首先,添加所需的依赖项到项目的pom.xml
文件中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
然后,在application.properties
或application.yml
文件中配置安全审计和监控:
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
management.endpoint.auditevents.enabled=true
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
auditevents:
enabled: true
这样配置后,可以通过访问/actuator/auditevents
端点来获取与令牌相关的审计事件信息。
这些组件一起工作,实现了OAuth2的认证和授权机制。下面我们将详细说明如何配置和使用这些组件。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client_id")
.secret("client_secret")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("read", "write")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
}
在上述代码中,我们通过
@EnableAuthorizationServer
注解启用了授权服务器,并继承了AuthorizationServerConfigurerAdapter
类来配置授权服务器。在configure
方法中,我们配置了一个简单的客户端,包括客户端ID、密钥、授权类型、作用域以及访问令牌和刷新令牌的有效期。在configure
方法中,我们还将authenticationManager
注入到AuthorizationServerEndpointsConfigurer
中,以便进行用户认证。
AuthorizationServerConfigurerAdapter
和ResourceServerConfigurerAdapter
类来实现自定义配置。@Configuration
@EnableAuthorizationServer
public class CustomAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// 自定义授权服务器的配置
}
@Configuration
@EnableResourceServer
public class CustomResourceServerConfig extends ResourceServerConfigurerAdapter {
// 自定义资源服务器的配置
}
在上述代码中,我们分别扩展了
AuthorizationServerConfigurerAdapter
和ResourceServerConfigurerAdapter
类,并使用@EnableAuthorizationServer
和@EnableResourceServer
注解启用了自定义的授权服务器和资源服务器。我们可以在相应的配置类中添加自定义的配置,例如定义访问规则、认证管理器等。
客户端向授权服务器发起认证请求,提供客户端ID、密钥、授权类型等信息。
// 客户端发起认证请求的代码示例
RestTemplate restTemplate = new RestTemplate();
// 构建认证请求参数
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "password");
params.add("username", "user");
params.add("password", "password");
params.add("client_id", "client_id");
params.add("client_secret", "client_secret");
// 发起认证请求
ResponseEntity<TokenResponse> response = restTemplate.postForEntity("http://localhost:8080/oauth/token", params, TokenResponse.class);
TokenResponse tokenResponse = response.getBody();
授权服务器验证客户端的身份和权限,并颁发访问令牌和刷新令牌。
// 配置授权服务器的代码示例
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client_id")
.secret("client_secret")
.authorizedGrantTypes("password", "refresh_token")
.scopes("read", "write")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
}
客户端使用访问令牌请求资源服务器获取受限资源。
// 客户端请求资源服务器的代码示例
RestTemplate restTemplate = new RestTemplate();
// 设置访问令牌
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + tokenResponse.getAccess_token());
HttpEntity<String> entity = new HttpEntity<>(headers);
// 请求受限资源
ResponseEntity<String> response = restTemplate.exchange("http://localhost:8080/api/resource", HttpMethod.GET, entity, String.class);
String resource = response.getBody();
资源服务器验证访问令牌的有效性和权限,并返回受限资源给客户端。
// 配置资源服务器的代码示例
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/resource").authenticated();
}
}
1.配置application.properties文件,添加GitHub OAuth2相关配置:(这里记得要换成你们自己的账号之类的配置)
spring.security.oauth2.client.registration.github.client-id=your-client-id
spring.security.oauth2.client.registration.github.client-secret=your-client-secret
spring.security.oauth2.client.registration.github.scope=user:email
spring.security.oauth2.client.registration.github.redirect-uri=http://localhost:8080/login/oauth2/code/github
spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize
spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token
spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user
2.创建一个自定义的登录页面(login.html),包含GitHub登录按钮:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<h2>Login</h2>
<a href="/login/github">Login with GitHub</a>
</body>
</html>
3.创建一个Controller处理登录请求和回调:
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/login/github")
public RedirectView loginWithGithub() {
return new RedirectView("/oauth2/authorization/github");
}
@GetMapping("/login/oauth2/code/github")
public String handleGithubCallback(@RequestParam("code") String code) {
// 处理GitHub回调逻辑
return "redirect:/home";
}
}
创建一个HomeController用于验证登录成功后的页面:
@Controller
public class HomeController {
@GetMapping("/home")
public String home() {
return "home";
}
}
创建一个WebSecurityConfigurerAdapter配置类,启用OAuth2登录:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password("{noop}password")
.roles("USER");
}
}
登录成功!
创建多个微服务 创建商城服务和商家管理后台服务的Spring Boot项目。可以使用Spring Initializr(https://start.spring.io/)来快速生成项目骨架。
配置Spring Security OAuth2的客户端 在商城服务和商家管理后台服务的配置文件(例如application.properties或application.yml)中,添加以下配置:
spring:
security:
oauth2:
client:
registration:
my-client: # 客户端ID,可以自定义
client-id: <your-client-id>
client-secret: <your-client-secret>
authorization-grant-type: client_credentials
scope: read, write
provider: my-provider # 授权服务器名称,可以自定义
provider:
my-provider:
token-uri: <authorization-server-token-uri>
请替换<your-client-id>
、<your-client-secret>
和<authorization-server-token-uri>
为实际值。这些值将根据你的授权服务器的配置而有所不同。
步骤3:创建授权服务器 创建一个独立的授权服务器,用于颁发访问令牌和验证客户端。可以使用Spring Security OAuth2和Spring Boot来实现授权服务器。
授权服务器:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("<your-client-id>")
.secret("{noop}<your-client-secret>")
.authorizedGrantTypes("client_credentials")
.scopes("read", "write");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
}
步骤4:配置微服务的授权服务器信息和访问令牌 在商城服务和商家管理后台服务的配置文件中,配置授权服务器的信息和访问令牌。
商城服务的配置文件示例:
spring:
security:
oauth2:
client:
registration:
my-client:
client-id: <your-client-id>
client-secret: <your-client-secret>
provider: my-provider
provider:
my-provider:
token-uri: <authorization-server-token-uri>
商家管理后台服务的配置文件示例:
spring:
security:
oauth2:
client:
registration:
my-client:
client-id: <your-client-id>
client-secret: <your-client-secret>
provider: my-provider
provider:
my-provider:
token-uri: <authorization-server-token-uri>
请替换
<your-client-id>
、<your-client-secret>
和<authorization-server-token-uri>
为您的实际值。
步骤5:配置微服务的安全规则 在商城服务和商家管理后台服务中,配置安全规则,包括访问规则和权限控制。
商城服务的安全配置示例:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/api/**").authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}
商家管理后台服务的安全配置示例:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/api/**").authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation("<authorization-server-issuer-uri>");
// 配置JwtDecoder,包括验证签名等
return jwtDecoder;
}
}
请替换<authorization-server-issuer-uri>
为你的授权服务器的颁发者URI。
上述代码示例将配置商家管理后台服务的安全规则。所有以/public/
开头的请求将被允许无需身份验证,而以/api/
开头的请求将需要进行身份验证。
JwtDecoder
bean,我们可以配置JWT解码器,以便验证JWT令牌的签名等信息import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
@Configuration
public class SecurityConfig {
@Bean
public JwtDecoder jwtDecoder() {
String issuerUri = "<authorization-server-issuer-uri>";
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(issuerUri + "/.well-known/jwks.json").build();
// 配置JwtDecoder,包括验证签名等
return jwtDecoder;
}
}
请将<authorization-server-issuer-uri>
替换为你的授权服务器的颁发者URI。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>5.x.x</version>
</dependency>