Step‑by‑Step Guide to Integrating Spring Boot with OAuth2.0 Authorization and Resource Servers

This article walks through building a complete OAuth2.0 solution in Spring Boot, covering core concepts, a comparison of the four grant types, project initialization, detailed configuration of the authorization and resource servers, custom login pages, end‑to‑end testing, advanced optimizations, and security best‑practice recommendations.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Step‑by‑Step Guide to Integrating Spring Boot with OAuth2.0 Authorization and Resource Servers

OAuth2.0 Core Concept

OAuth2.0 solves the “authorization instead of authentication” problem, allowing a user to log in to a third‑party application (e.g., WeChat) without exposing the original password.

Grant Types Comparison

Authorization Code – suitable for web and mobile apps; high security; ★★★★★ usage.

Password – for trusted internal applications; medium security; ★★☆☆☆ usage.

Client Credentials – for server‑to‑server calls; medium security; ★★★☆☆ usage.

Implicit (Simplified) – for pure front‑end apps; low security; ★☆☆☆☆ usage.

Project Goal

Authorization Server – issues tokens.

Resource Server – validates tokens and serves protected APIs.

Client Application – obtains tokens and accesses resources.

Project Initialization

Dependencies

<dependencies>
  <!-- Spring Boot Web -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <!-- OAuth2 Authorization Server -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
  </dependency>

  <!-- OAuth2 Resource Server -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
  </dependency>

  <!-- JPA for client and user data -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
  </dependency>

  <!-- Lombok (optional) -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
</dependencies>

Database Configuration (application.yml)

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/oauth2_demo?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
  server:
    port: 8080

Authorization Server Implementation

Configuration Class

@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.formLogin(Customizer.withDefaults()).build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("my-client")
            .clientSecret("{noop}my-secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
            .redirectUri("http://127.0.0.1:8080/login/oauth2/code/my-client")
            .redirectUri("http://127.0.0.1:8080/authorized")
            .scope(OidcScopes.OPENID)
            .scope("read")
            .scope("write")
            .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
            .build();
        return new InMemoryRegisteredClientRepository(client);
    }

    @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() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            return keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }
}

User Details Service (In‑Memory Demo)

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Map<String, UserDetails> users = new HashMap<>();
        users.put("admin", User.withUsername("admin").password("{noop}admin123").roles("ADMIN", "USER").authorities("read", "write").build());
        users.put("user", User.withUsername("user").password("{noop}user123").roles("USER").authorities("read").build());
        UserDetails user = users.get(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }
        return user;
    }
}

Custom Login Page (Thymeleaf)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>OAuth2 Authorization Login</title>
    <style>
        body {font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto;}
        .form-group {margin-bottom: 15px;}
        label {display: block; margin-bottom: 5px;}
        input {width: 100%; padding: 8px; box-sizing: border-box;}
        button {background: #007bff; color: white; border: none; padding: 10px 20px; cursor: pointer;}
        .error {color: red; margin-top: 10px;}
    </style>
</head>
<body>
    <h2>授权登录</h2>
    <div th:if="${param.error}" class="error">用户名或密码错误!</div>
    <form th:action="@{/login}" method="post">
        <div class="form-group">
            <label>用户名:</label>
            <input type="text" name="username" required/>
        </div>
        <div class="form-group">
            <label>密码:</label>
            <input type="password" name="password" required/>
        </div>
        <button type="submit">登录并授权</button>
    </form>
</body>
</html>

Login Controller

@Controller
public class LoginController {
    @GetMapping("/login")
    public String login() { return "login"; }

    @GetMapping("/authorized")
    @ResponseBody
    public String authorized(@RequestParam(required = false) String code,
                             @RequestParam(required = false) String error) {
        if (error != null) {
            return "授权失败: " + error;
        }
        return "授权成功!授权码: " + code + "
请使用此授权码获取访问令牌。";
    }
}

Resource Server Implementation

Configuration

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig {
    @Bean
    @Order(2)
    public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .csrf(csrf -> csrf.disable());
        return http.build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        String jwkSetUri = "http://localhost:8080/oauth2/jwks";
        return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
    }
}

Protected APIs

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

    @GetMapping("/user/info")
    @PreAuthorize("hasAuthority('SCOPE_read')")
    public ResponseEntity<?> getUserInfo(@AuthenticationPrincipal Jwt jwt) {
        String username = jwt.getClaim("sub");
        String email = jwt.getClaim("email");
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("username", username);
        userInfo.put("email", email != null ? email : username + "@example.com");
        userInfo.put("userId", jwt.getClaim("user_id"));
        userInfo.put("authorities", jwt.getClaim("scope"));
        userInfo.put("issuedAt", jwt.getIssuedAt());
        userInfo.put("expiresAt", jwt.getExpiresAt());
        return ResponseEntity.ok(Map.of("code", 200, "message", "User info retrieved", "data", userInfo));
    }

    @PostMapping("/resource")
    @PreAuthorize("hasAuthority('SCOPE_write')")
    public ResponseEntity<?> createResource(@RequestBody Map<String, Object> resource,
                                            @AuthenticationPrincipal Jwt jwt) {
        String username = jwt.getClaim("sub");
        Map<String, Object> response = new HashMap<>();
        response.put("id", UUID.randomUUID().toString());
        response.put("name", resource.get("name"));
        response.put("creator", username);
        response.put("createdAt", new Date());
        response.put("message", "Resource created successfully");
        return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("code", 201, "message", "Resource created", "data", response));
    }

    @GetMapping("/public/hello")
    public ResponseEntity<?> publicHello() {
        return ResponseEntity.ok(Map.of("code", 200, "message", "Public endpoint, no authentication required", "timestamp", new Date()));
    }

    @GetMapping("/admin/dashboard")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> adminDashboard(@AuthenticationPrincipal Jwt jwt) {
        return ResponseEntity.ok(Map.of(
            "code", 200,
            "message", "Welcome admin: " + jwt.getClaim("sub"),
            "adminData", Map.of("totalUsers", 1500, "activeSessions", 42, "systemStatus", "NORMAL")));
    }
}

Global Exception Handling

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<?> handleAuthenticationException(AuthenticationException e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of(
            "code", 401,
            "message", "认证失败:" + e.getMessage(),
            "timestamp", new Date()));
    }

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<?> handleAccessDeniedException(AccessDeniedException e) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of(
            "code", 403,
            "message", "权限不足,无法访问此资源",
            "timestamp", new Date()));
    }

    @ExceptionHandler(JwtException.class)
    public ResponseEntity<?> handleJwtException(JwtException e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of(
            "code", 401,
            "message", "Token无效或已过期",
            "error", e.getMessage(),
            "timestamp", new Date()));
    }
}

Testing the Complete OAuth2 Flow

1️⃣ Obtain an authorization code by opening the following URL in a browser:

http://localhost:8080/oauth2/authorize?response_type=code&client_id=my-client&redirect_uri=http://127.0.0.1:8080/authorized&scope=read

Log in with either admin/admin123 or user/user123. After consent, the browser redirects to the callback URL displaying the code.

2️⃣ Exchange the code for an access token (curl example):

curl --location 'http://localhost:8080/oauth2/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'Authorization: Basic bXktY2xpZW50Om15LXNlY3JldA==' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'code=YOUR_AUTHORIZATION_CODE' \
  --data-urlencode 'redirect_uri=http://127.0.0.1:8080/authorized'

3️⃣ Call a protected API with the obtained token:

curl --location 'http://localhost:8080/api/user/info' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'

4️⃣ Client Credentials grant (machine‑to‑machine):

curl --location 'http://localhost:8080/oauth2/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'Authorization: Basic bXktY2xpZW50Om15LXNlY3JldA==' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'scope=read'

5️⃣ Refresh token when the access token expires:

curl --location 'http://localhost:8080/oauth2/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'Authorization: Basic bXktY2xpZW50Om15LXNlY3JldA==' \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode 'refresh_token=YOUR_REFRESH_TOKEN'

Advanced Configuration & Optimization

Persisting Clients in a Database

@Entity
@Table(name = "oauth2_clients")
@Data
public class OAuth2Client {
    @Id
    private String id;
    @Column(nullable = false, unique = true)
    private String clientId;
    private String clientSecret;
    private String authenticationMethods; // comma‑separated
    private String authorizationGrantTypes; // comma‑separated
    private String redirectUris; // comma‑separated
    private String scopes; // comma‑separated
    private Boolean requireProofKey = false;
    private Boolean requireAuthorizationConsent = false;
    @CreationTimestamp
    private LocalDateTime createTime;
    @UpdateTimestamp
    private LocalDateTime updateTime;
}
@Service
@Transactional
public class JpaRegisteredClientRepository implements RegisteredClientRepository {
    @Autowired
    private OAuth2ClientRepository clientRepository;

    @Override
    public void save(RegisteredClient registeredClient) {
        // Persist to database
    }

    @Override
    public RegisteredClient findById(String id) {
        // Retrieve from database
        return null;
    }

    @Override
    public RegisteredClient findByClientId(String clientId) {
        // Retrieve from database
        return null;
    }
}

Custom Token Enhancer

@Component
public class CustomTokenEnhancer implements OAuth2TokenCustomizer<JwtEncodingContext> {
    @Override
    public void customize(JwtEncodingContext context) {
        if (context.getPrincipal() instanceof UsernamePasswordAuthenticationToken) {
            Authentication authentication = context.getPrincipal();
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            context.getClaims().claims(claims -> {
                claims.put("user_id", generateUserId(userDetails.getUsername()));
                claims.put("email", userDetails.getUsername() + "@company.com");
                claims.put("company", "MyCompany");
                claims.put("department", "IT");
            });
        }
    }

    private String generateUserId(String username) {
        return "user_" + username.hashCode();
    }
}
@Bean
public OAuth2AuthorizationServerConfiguration.OAuth2AuthorizationServerConfigurer authorizationServerConfigurer(
        RegisteredClientRepository registeredClientRepository,
        CustomTokenEnhancer customTokenEnhancer) {
    return OAuth2AuthorizationServerConfiguration
            .authorizationServerConfigurer(registeredClientRepository)
            .tokenEnhancer(customTokenEnhancer);
}

Token Lifetimes

@Bean
public TokenSettings tokenSettings() {
    return TokenSettings.builder()
        .accessTokenTimeToLive(Duration.ofHours(2))
        .refreshTokenTimeToLive(Duration.ofDays(7))
        .authorizationCodeTimeToLive(Duration.ofMinutes(5))
        .reuseRefreshTokens(false)
        .build();
}

Security Best Practices

Never use {noop} password encoder in production. Replace with BCryptPasswordEncoder:

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

Enforce HTTPS to protect tokens in transit.

Restrict redirect URIs to exact trusted endpoints.

Rotate JWK signing keys regularly.

Monitoring & Auditing

@Component
public class OAuth2AuditLogger {
    private static final Logger log = LoggerFactory.getLogger("OAUTH2_AUDIT");

    @EventListener
    public void onAuthorizationSuccess(AuthorizationSuccessEvent event) {
        log.info("授权成功 - 客户端: {}, 用户: {}, 范围: {}",
                event.getAuthorization().getRegisteredClientId(),
                event.getAuthorization().getPrincipalName(),
                event.getAuthorization().getAuthorizedScopes());
    }

    @EventListener
    public void onAuthorizationFailure(AuthorizationErrorEvent event) {
        log.warn("授权失败 - 错误: {}, 客户端: {}",
                event.getError(),
                event.getAuthorization().getRegisteredClientId());
    }

    @EventListener
    public void onTokenIssued(OAuth2TokenIssuedEvent event) {
        log.info("Token签发 - 类型: {}, 客户端: {}",
                event.getToken().getClass().getSimpleName(),
                event.getAuthorization().getRegisteredClientId());
    }
}

Performance Optimizations

Cache authorizations in Redis :

@Bean
public OAuth2AuthorizationService authorizationService(
        RegisteredClientRepository registeredClientRepository,
        RedisConnectionFactory redisConnectionFactory) {
    return new RedisOAuth2AuthorizationService(redisConnectionFactory, registeredClientRepository);
}

Enable reactive resource server support for non‑blocking I/O:

@Configuration
@EnableWebFluxSecurity
public class ReactiveResourceServerConfig {
    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.authorizeExchange(exchanges -> exchanges.anyExchange().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }
}

FAQ

Q1: How to support multiple authentication methods?

Configure them on RegisteredClient via .clientAuthenticationMethods(...):

.clientAuthenticationMethods(methods -> {
    methods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
    methods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
    methods.add(ClientAuthenticationMethod.CLIENT_SECRET_JWT);
    methods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
})

Q2: How to implement Single Sign‑On (SSO)?

@Configuration
@EnableWebSecurity
public class OAuth2LoginConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/home"))
            .authorizeHttpRequests(authz -> authz.anyRequest().authenticated());
        return http.build();
    }
}

Q3: How to integrate third‑party logins (GitHub, WeChat, etc.)?

Add the provider configuration under spring.security.oauth2.client.registration and spring.security.oauth2.client.provider in application.yml:

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: YOUR_GITHUB_CLIENT_ID
            client-secret: YOUR_GITHUB_CLIENT_SECRET
            authorization-grant-type: authorization_code
            scope: read:user
          wechat:
            client-id: YOUR_WECHAT_APPID
            client-secret: YOUR_WECHAT_SECRET
            authorization-grant-type: authorization_code
            scope: snsapi_userinfo
            client-name: 微信
            provider: wechat
        provider:
          wechat:
            authorization-uri: https://open.weixin.qq.com/connect/oauth2/authorize
            token-uri: https://api.weixin.qq.com/sns/oauth2/access_token
            user-info-uri: https://api.weixin.qq.com/sns/userinfo

Conclusion

Authorization Server – registers clients, authenticates users, issues JWT tokens; supports multiple grant types.

Resource Server – validates JWT tokens, protects API endpoints with method‑level security.

JWT Tokens – stateless, suitable for distributed systems.

Spring Security OAuth2 provides a complete, configurable solution; start with the in‑memory example, then replace with persistent storage, custom token claims, and production‑grade security settings.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaspring-bootsecurityjwtoauth2spring securityauthorization-serverresource-server
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.