云计算百科
云计算领域专业知识百科平台

从零到一:用Java和Spring Security构建OAuth2授权服务器

引言

在当今的互联网生态中,安全认证与授权机制对于保护用户数据和系统资源至关重要。OAuth2作为一种行业标准的授权框架,被广泛应用于各类Web应用、移动应用和API服务中。本文将带领读者从零开始,使用Java和Spring Security框架构建一个功能完整的OAuth2授权服务器,深入理解OAuth2的核心概念和实现细节。

OAuth2基础知识

OAuth2是什么?

OAuth2(Open Authorization 2.0)是一个开放标准的授权协议,允许第三方应用在不获取用户凭证的情况下,获得对用户资源的有限访问权限。它解决了传统认证方式中的安全隐患,如密码共享和过度授权等问题。

OAuth2的角色

OAuth2定义了四个关键角色:

  • 资源所有者(Resource Owner):通常是用户,拥有受保护资源的实体。
  • 客户端(Client):请求访问资源的应用程序。
  • 授权服务器(Authorization Server):验证资源所有者身份并颁发访问令牌。
  • 资源服务器(Resource Server):托管受保护资源的服务器,接受并验证访问令牌。
  • OAuth2的授权流程

    OAuth2支持多种授权流程,适用于不同场景:

  • 授权码模式(Authorization Code):最完整、最安全的流程,适用于有后端的Web应用。
  • 简化模式(Implicit):适用于无后端的单页应用。
  • 密码模式(Resource Owner Password Credentials):适用于高度可信的应用。
  • 客户端凭证模式(Client Credentials):适用于服务器间通信。
  • 项目准备

    环境要求

    • JDK 11+
    • Maven 3.6+
    • Spring Boot 2.6.x
    • Spring Security 5.6.x
    • Spring Authorization Server 0.3.x

    项目结构

    oauth2-server/
    ├── src/
    │ ├── main/
    │ │ ├── java/
    │ │ │ └── com/
    │ │ │ └── example/
    │ │ │ └── oauth2server/
    │ │ │ ├── config/
    │ │ │ ├── controller/
    │ │ │ ├── entity/
    │ │ │ ├── repository/
    │ │ │ ├── service/
    │ │ │ └── OAuth2ServerApplication.java
    │ │ └── resources/
    │ │ ├── templates/
    │ │ ├── static/
    │ │ └── application.yml
    │ └── test/
    ├── pom.xml
    └── README.md

    Maven依赖配置

    <dependencies>
    <!– Spring Boot –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <!– Spring Authorization Server –>
    <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>0.3.1</version>
    </dependency>

    <!– Database –>
    <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    </dependency>

    <!– Utilities –>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>
    </dependencies>

    实现授权服务器

    步骤1:创建基础应用

    首先,创建一个Spring Boot应用作为我们的起点:

    package com.example.oauth2server;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class OAuth2ServerApplication {
    public static void main(String[] args) {
    SpringApplication.run(OAuth2ServerApplication.class, args);
    }
    }

    步骤2:配置数据库

    在application.yml中配置数据库连接:

    spring:
    datasource:
    url: jdbc:h2:mem:oauth2db
    driver-class-name: org.h2.Driver
    username: sa
    password: password
    jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
    ddl-auto: update
    show-sql: true
    h2:
    console:
    enabled: true
    path: /h2console

    步骤3:创建用户实体和存储

    package com.example.oauth2server.entity;

    import lombok.Data;
    import javax.persistence.*;
    import java.util.Set;

    @Entity
    @Data
    public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;

    private String password;

    @ElementCollection(fetch = FetchType.EAGER)
    private Set<String> roles;

    private boolean enabled = true;
    }

    创建用户存储库:

    package com.example.oauth2server.repository;

    import com.example.oauth2server.entity.User;
    import org.springframework.data.jpa.repository.JpaRepository;
    import java.util.Optional;

    public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    }

    步骤4:实现用户服务

    package com.example.oauth2server.service;

    import com.example.oauth2server.entity.User;
    import com.example.oauth2server.repository.UserRepository;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;

    import java.util.stream.Collectors;

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
    this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByUsername(username)
    .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));

    return new org.springframework.security.core.userdetails.User(
    user.getUsername(),
    user.getPassword(),
    user.isEnabled(),
    true,
    true,
    true,
    user.getRoles().stream()
    .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
    .collect(Collectors.toSet())
    );
    }
    }

    步骤5:配置安全设置

    package com.example.oauth2server.config;

    import com.example.oauth2server.service.UserDetailsServiceImpl;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.SecurityFilterChain;

    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {

    private final UserDetailsServiceImpl userDetailsService;

    public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
    this.userDetailsService = userDetailsService;
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http
    .authorizeHttpRequests(authorize ->
    authorize
    .antMatchers("/h2-console/**").permitAll()
    .anyRequest().authenticated()
    )
    .formLogin()
    .and()
    .csrf().ignoringAntMatchers("/h2-console/**")
    .and()
    .headers().frameOptions().sameOrigin();

    return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
    }
    }

    步骤6:配置OAuth2授权服务器

    package com.example.oauth2server.config;

    import com.nimbusds.jose.jwk.JWKSet;
    import com.nimbusds.jose.jwk.RSAKey;
    import com.nimbusds.jose.jwk.source.JWKSource;
    import com.nimbusds.jose.proc.SecurityContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.Ordered;
    import org.springframework.core.annotation.Order;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
    import org.springframework.security.oauth2.core.AuthorizationGrantType;
    import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
    import org.springframework.security.oauth2.core.oidc.OidcScopes;
    import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
    import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
    import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
    import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
    import org.springframework.security.web.SecurityFilterChain;
    import org.springframework.security.web.util.matcher.RequestMatcher;

    import java.security.KeyPair;
    import java.security.KeyPairGenerator;
    import java.security.interfaces.RSAPrivateKey;
    import java.security.interfaces.RSAPublicKey;
    import java.util.UUID;

    @Configuration
    public class AuthorizationServerConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
    new OAuth2AuthorizationServerConfigurer<>();

    RequestMatcher endpointsMatcher = authorizationServerConfigurer
    .getEndpointsMatcher();

    http
    .requestMatcher(endpointsMatcher)
    .authorizeRequests(authorizeRequests ->
    authorizeRequests.anyRequest().authenticated()
    )
    .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
    .apply(authorizationServerConfigurer);

    return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
    .clientId("client")
    .clientSecret("$2a$10$jdJGhzsiIqYFpjJiYWMl/eKDOd8vdyQis2aynmFN0dgJ53XvpzzwC") // "secret" encoded
    .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
    .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
    .redirectUri("http://127.0.0.1:8080/login/oauth2/code/client")
    .scope(OidcScopes.OPENID)
    .scope("read")
    .scope("write")
    .build();

    return new InMemoryRegisteredClientRepository(registeredClient);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
    RSAKey rsaKey = generateRsa();
    JWKSet jwkSet = new JWKSet(rsaKey);
    return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    private static RSAKey generateRsa() {
    KeyPair keyPair = generateRsaKey();
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    return new RSAKey.Builder(publicKey)
    .privateKey(privateKey)
    .keyID(UUID.randomUUID().toString())
    .build();
    }

    private static KeyPair generateRsaKey() {
    KeyPair keyPair;
    try {
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    keyPairGenerator.initialize(2048);
    keyPair = keyPairGenerator.generateKeyPair();
    } catch (Exception ex) {
    throw new IllegalStateException(ex);
    }
    return keyPair;
    }

    @Bean
    public ProviderSettings providerSettings() {
    return ProviderSettings.builder()
    .issuer("http://localhost:9000")
    .build();
    }
    }

    步骤7:初始化测试数据

    创建一个数据初始化器:

    package com.example.oauth2server.config;

    import com.example.oauth2server.entity.User;
    import com.example.oauth2server.repository.UserRepository;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.crypto.password.PasswordEncoder;

    import java.util.Set;

    @Configuration
    public class DataInitializer {

    @Bean
    public CommandLineRunner initData(UserRepository userRepository, PasswordEncoder passwordEncoder) {
    return args -> {
    User admin = new User();
    admin.setUsername("admin");
    admin.setPassword(passwordEncoder.encode("admin"));
    admin.setRoles(Set.of("ADMIN", "USER"));

    User user = new User();
    user.setUsername("user");
    user.setPassword(passwordEncoder.encode("password"));
    user.setRoles(Set.of("USER"));

    userRepository.save(admin);
    userRepository.save(user);
    };
    }
    }

    步骤8:配置应用属性

    在application.yml中添加服务器端口配置:

    server:
    port: 9000

    测试授权服务器

    授权码流程测试

  • 请求授权码:

    访问以下URL(可以在浏览器中打开):

    http://localhost:9000/oauth2/authorize?response_type=code&client_id=client&scope=read&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/client

    系统会要求登录(使用我们创建的用户凭据),然后请求授权。授权后,系统会重定向到指定的URI,并附带授权码。

  • 使用授权码获取令牌:

    使用curl或Postman发送POST请求:

    curl -X POST \\
    http://localhost:9000/oauth2/token \\
    -H "Content-Type: application/x-www-form-urlencoded" \\
    -H "Authorization: Basic Y2xpZW50OnNlY3JldA==" \\
    -d "grant_type=authorization_code&code=YOUR_AUTHORIZATION_CODE&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/client"

    注意:YOUR_AUTHORIZATION_CODE需要替换为上一步获取的授权码。

  • 使用访问令牌访问资源:

    使用获取到的访问令牌访问受保护资源:

    curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" http://localhost:9000/api/resource

  • 扩展功能

    添加资源服务器

    创建一个简单的资源API:

    package com.example.oauth2server.controller;

    import org.springframework.security.core.annotation.AuthenticationPrincipal;
    import org.springframework.security.oauth2.jwt.Jwt;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.Collections;
    import java.util.Map;

    @RestController
    @RequestMapping("/api")
    public class ResourceController {

    @GetMapping("/resource")
    public Map<String, Object> resource(@AuthenticationPrincipal Jwt jwt) {
    return Collections.singletonMap("message",
    "Protected resource accessed by: " + jwt.getSubject());
    }
    }

    配置资源服务器安全设置:

    package com.example.oauth2server.config;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.web.SecurityFilterChain;

    @Configuration
    @EnableWebSecurity
    public class ResourceServerConfig {

    @Bean
    @Order(3)
    public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
    http
    .requestMatchers()
    .antMatchers("/api/**")
    .and()
    .authorizeRequests()
    .anyRequest().authenticated()
    .and()
    .oauth2ResourceServer()
    .jwt();

    return http.build();
    }
    }

    实现令牌撤销

    添加令牌撤销端点:

    package com.example.oauth2server.controller;

    import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
    import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    public class TokenController {

    private final OAuth2AuthorizationService authorizationService;

    public TokenController(OAuth2AuthorizationService authorizationService) {
    this.authorizationService = authorizationService;
    }

    @PostMapping("/oauth2/revoke")
    public void revokeToken(@RequestParam("token") String token,
    @RequestParam("token_type_hint") String tokenTypeHint) {
    OAuth2TokenType tokenType = "access_token".equals(tokenTypeHint)
    ? OAuth2TokenType.ACCESS_TOKEN
    : OAuth2TokenType.REFRESH_TOKEN;

    authorizationService.findByToken(token, tokenType)
    .ifPresent(authorization -> {
    authorizationService.remove(authorization);
    });
    }
    }

    自定义授权同意页面

    创建一个Thymeleaf模板用于授权同意页面:

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
    <head>
    <meta charset="UTF-8">
    <title>授权确认</title>
    <style>
    body {
    font-family: Arial, sans-serif;
    max-width: 600px;
    margin: 0 auto;
    padding: 20px;
    }
    .container {
    border: 1px solid #ddd;
    border-radius: 5px;
    padding: 20px;
    margin-top: 20px;
    }
    .btn {
    display: inline-block;
    padding: 10px 15px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    text-decoration: none;
    margin-right: 10px;
    }
    .btn-cancel {
    background-color: #f44336;
    }
    .scopes {
    margin: 15px 0;
    }
    .scope-item {
    margin: 5px 0;
    }
    </style>
    </head>
    <body>
    <div class="container">
    <h1>授权请求</h1>
    <p>
    客户端 <strong th:text="${clientId}"></strong> 请求访问您的账户
    </p>

    <div class="scopes">
    <p>请求的权限范围:</p>
    <div th:each="scope : ${scopes}" class="scope-item">
    <input type="checkbox" th:id="${scope}" th:name="scope" th:value="${scope}" checked />
    <label th:for="${scope}" th:text="${scope}"></label>
    </div>
    </div>

    <form method="post" th:action="${authorizationUri}">
    <input type="hidden" name="client_id" th:value="${clientId}">
    <input type="hidden" name="state" th:value="${state}">

    <div>
    <button type="submit" name="consent" value="approve" class="btn">授权</button>
    <button type="submit" name="consent" value="deny" class="btn btn-cancel">拒绝</button>
    </div>
    </form>
    </div>
    </body>
    </html>

    创建控制器处理授权同意请求:

    package com.example.oauth2server.controller;

    import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
    import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
    import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
    import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
    import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;

    import java.security.Principal;
    import java.util.Set;

    @Controller
    public class AuthorizationConsentController {

    private final RegisteredClientRepository registeredClientRepository;
    private final OAuth2AuthorizationConsentService authorizationConsentService;

    public AuthorizationConsentController(
    RegisteredClientRepository registeredClientRepository,
    OAuth2AuthorizationConsentService authorizationConsentService) {
    this.registeredClientRepository = registeredClientRepository;
    this.authorizationConsentService = authorizationConsentService;
    }

    @GetMapping("/oauth2/consent")
    public String consent(
    Principal principal,
    Model model,
    @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
    @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
    @RequestParam(OAuth2ParameterNames.STATE) String state) {

    RegisteredClient client = this.registeredClientRepository.findByClientId(clientId);
    OAuth2AuthorizationConsent consent = this.authorizationConsentService.findById(
    clientId, principal.getName());

    Set<String> scopesToApprove = Set.of(scope.split(" "));

    model.addAttribute("clientId", clientId);
    model.addAttribute("state", state);
    model.addAttribute("scopes", scopesToApprove);
    model.addAttribute("authorizationUri", "/oauth2/authorize");

    return "consent";
    }
    }

    安全最佳实践

    在实现OAuth2授权服务器时,应遵循以下安全最佳实践:

  • 使用HTTPS:在生产环境中,始终使用HTTPS保护所有通信。

  • 安全存储客户端密钥:客户端密钥应该使用强密码哈希算法(如BCrypt)进行加密存储。

  • 实施PKCE:对于公共客户端(如SPA和移动应用),使用PKCE(Proof Key for Code Exchange)增强安全性。

  • 限制令牌范围和生命周期:根据实际需求限制访问令牌的范围和有效期。

  • 实施令牌撤销:提供令牌撤销机制,允许用户或管理员在需要时撤销访问权限。

  • 监控和审计:实施日志记录和监控,以便及时发现可疑活动。

  • 结论

    通过本文,我们从零开始构建了一个功能完整的OAuth2授权服务器。我们深入了解了OAuth2的核心概念,并使用Spring Security和Spring Authorization Server实现了各种授权流程和扩展功能。

    这个授权服务器可以作为您实际项目的起点,根据具体需求进行定制和扩展。随着安全需求的不断演变,持续关注OAuth2和Spring Security的最新发展,及时更新您的实现,是确保系统安全的关键。

    参考资料

    • OAuth 2.0 规范
    • Spring Authorization Server 文档
    • Spring Security 文档
    • RFC 6749 – OAuth 2.0 授权框架
    • RFC 7636 – PKCE
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 从零到一:用Java和Spring Security构建OAuth2授权服务器
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!