跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Spring Boot 2.0 整合 Spring Security OAuth2

Spring Boot 2.0 整合 Spring Security OAuth2 的完整流程,涵盖 OAuth2 核心概念与授权模式、项目环境搭建与 Maven 依赖配置、数据库表结构设计(用户、角色、客户端)、JPA 实体与 Repository 层实现、Spring Security 安全配置、授权服务器与资源服务器配置、JWT 令牌生成与验证、RESTful API 控制器开发以及全局异常处理。文中还提供了 Redis 令牌存储方案、测试数据初始化脚本及 curl 测试命令,并总结了生产环境下的安全最佳实践。

奇形怪状发布于 2026/3/25更新于 2026/10/1914K 浏览

1. OAuth2 基础概念与原理

1.1 OAuth2 是什么

OAuth 2.0(开放授权 2.0)是一个行业标准的授权协议,它允许用户在不将用户名和密码提供给第三方应用的情况下,授权第三方应用访问用户存储在服务提供方的资源。

1.2 OAuth2 核心角色
  • 资源所有者 (Resource Owner):能够授权访问受保护资源的实体,通常是最终用户
  • 客户端 (Client):请求访问受保护资源的应用程序
  • 授权服务器 (Authorization Server):在认证资源所有者并获取授权后,向客户端颁发访问令牌的服务器
  • 资源服务器 (Resource Server):托管受保护资源的服务器,能够接受和响应使用访问令牌的受保护资源请求
1.3 OAuth2 授权模式
  1. 授权码模式 (Authorization Code):最安全、最常用的模式,适用于有后端的 Web 应用
  2. 简化模式 (Implicit):适用于纯前端 SPA 应用
  3. 密码模式 (Resource Owner Password Credentials):适用于信任的客户端,如官方应用
  4. 客户端模式 (Client Credentials):适用于客户端访问自己的资源

2. 环境准备与项目搭建

2.1 创建 Spring Boot 项目

使用 Spring Initializr 创建项目,选择以下依赖:

  • Spring Web
  • Spring Security
  • Spring Security OAuth2
  • Spring Data JPA
  • MySQL Driver
  • Lombok
2.2 Maven 依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-security-oauth2-demo</artifactId>
    <version>1.0.0</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- Spring Boot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- OAuth2 自动配置 -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>
        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- JJWT for JWT tokens -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3. 数据库设计

3.1 用户表结构
CREATE TABLE `sys_user` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `username` varchar(50) NOT NULL COMMENT '用户名',
    `password` varchar(100) NOT NULL COMMENT '密码',
    `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
    `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
    `enabled` tinyint(1) DEFAULT '1' COMMENT '状态:1-有效,0-禁用',
    `account_non_expired` tinyint(1) DEFAULT '1' COMMENT '账户是否过期',
    `credentials_non_expired` tinyint(1) DEFAULT '1' COMMENT '密码是否过期',
    `account_non_locked` tinyint(1) DEFAULT '1' COMMENT '账户是否锁定',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
3.2 角色表结构
CREATE TABLE `sys_role` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `role_name` varchar(50) NOT NULL COMMENT '角色名称',
    `role_code` varchar(50) NOT NULL COMMENT '角色编码',
    `description` varchar(100) DEFAULT NULL COMMENT '描述',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
3.3 用户角色关联表
CREATE TABLE `sys_user_role` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `user_id` bigint(20) NOT NULL COMMENT '用户 ID',
    `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`),
    KEY `idx_user_id` (`user_id`),
    KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
3.4 OAuth2 客户端表
CREATE TABLE `oauth_client_details` (
    `client_id` varchar(256) NOT NULL COMMENT '客户端 ID',
    `resource_ids` varchar(256) DEFAULT NULL COMMENT '资源 ID',
    `client_secret` varchar(256) DEFAULT NULL COMMENT '客户端密钥',
    `scope` varchar(256) DEFAULT NULL COMMENT '权限范围',
    `authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '授权类型',
    `web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '重定向 URI',
    `authorities` varchar(256) DEFAULT NULL COMMENT '权限',
    `access_token_validity` int(11) DEFAULT NULL COMMENT '访问令牌有效期',
    `refresh_token_validity` int(11) DEFAULT NULL COMMENT '刷新令牌有效期',
    `additional_information` varchar(4096) DEFAULT NULL COMMENT '附加信息',
    `autoapprove` varchar(256) DEFAULT NULL COMMENT '自动批准',
    PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OAuth2 客户端表';

4. 实体类设计

4.1 用户实体
package com.example.oauth2.entity;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Data
@Entity
@Table(name = "sys_user")
public class SysUser implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "username", unique = true, nullable = false, length = 50)
    private String username;

    @Column(name = "password", nullable = false, length = 100)
    private String password;

    @Column(name = "email", length = 100)
    private String email;

    @Column(name = "phone", length = 20)
    private String phone;

    @Column(name = "enabled")
    private Boolean enabled = true;

    @Column(name = "account_non_expired")
    private Boolean accountNonExpired = true;

    @Column(name = "credentials_non_expired")
    private Boolean credentialsNonExpired = true;

    @Column(name = "account_non_locked")
    private Boolean accountNonLocked = true;

    @Column(name = "create_time")
    private Date createTime;

    @Column(name = "update_time")
    private Date updateTime;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
        name = "sys_user_role",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private List<SysRole> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority(role.getRoleCode()))
            .collect(Collectors.toList());
    }

    @Override
    public boolean isAccountNonExpired() { return accountNonExpired; }

    @Override
    public boolean isAccountNonLocked() { return accountNonLocked; }

    @Override
    public boolean isCredentialsNonExpired() { return credentialsNonExpired; }

    @Override
    public boolean isEnabled() { return enabled; }
}
4.2 角色实体
package com.example.oauth2.entity;
import lombok.Data;
import javax.persistence.*;
import java.util.Date;

@Data
@Entity
@Table(name = "sys_role")
public class SysRole {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "role_name", nullable = false, length = 50)
    private String roleName;

    @Column(name = "role_code", nullable = false, length = 50)
    private String roleCode;

    @Column(name = "description", length = 100)
    private String description;

    @Column(name = "create_time")
    private Date createTime;
}

5. 数据访问层

5.1 用户 Repository
package com.example.oauth2.repository;
import com.example.oauth2.entity.SysUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<SysUser, Long> {
    Optional<SysUser> findByUsername(String username);
    Boolean existsByUsername(String username);
    Boolean existsByEmail(String email);

    @Query("SELECT u FROM SysUser u LEFT JOIN FETCH u.roles WHERE u.username = :username")
    Optional<SysUser> findByUsernameWithRoles(@Param("username") String username);
}
5.2 角色 Repository
package com.example.oauth2.repository;
import com.example.oauth2.entity.SysRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface RoleRepository extends JpaRepository<SysRole, Long> {
    Optional<SysRole> findByRoleCode(String roleCode);
}

6. 安全配置

6.1 Spring Security 配置
package com.example.oauth2.config;
import com.example.oauth2.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    /**
     * 密码编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证管理器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置用户认证
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }

    /**
     * HTTP 安全配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/oauth/**", "/login/**", "/logout/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll()
            .and()
            .csrf().disable();
    }
}
6.2 用户详情服务
package com.example.oauth2.service;
import com.example.oauth2.entity.SysUser;
import com.example.oauth2.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = userRepository.findByUsernameWithRoles(username)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在:" + username));
        log.info("用户 {} 登录成功,角色:{}", username, user.getRoles().stream().map(role -> role.getRoleCode()).toArray());
        return user;
    }
}

7. OAuth2 授权服务器配置

7.1 授权服务器配置
package com.example.oauth2.config;
import com.example.oauth2.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import javax.sql.DataSource;
import java.util.Arrays;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * 配置客户端详情服务
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 使用 JDBC 存储客户端信息
        clients.withClientDetails(jdbcClientDetailsService());
        // 内存方式配置客户端(测试用)
        // clients.inMemory()
        //     .withClient("client")
        //     .secret(passwordEncoder.encode("secret"))
        //     .authorizedGrantTypes("password", "refresh_token")
        //     .scopes("all")
        //     .accessTokenValiditySeconds(3600)
        //     .refreshTokenValiditySeconds(86400);
    }

    /**
     * 配置令牌端点安全约束
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
            .tokenKeyAccess("permitAll()") // 公开/oauth/token_key 接口
            .checkTokenAccess("isAuthenticated()") // 认证后可访问/oauth/check_token
            .allowFormAuthenticationForClients(); // 允许表单认证
    }

    /**
     * 配置授权端点
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 令牌增强链
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));
        endpoints
            .authenticationManager(authenticationManager) // 认证管理器
            .userDetailsService(userDetailsService) // 用户详情服务
            .tokenStore(tokenStore()) // 令牌存储方式
            .tokenEnhancer(tokenEnhancerChain) // 令牌增强
            .accessTokenConverter(jwtAccessTokenConverter()) // JWT 转换器
            .reuseRefreshTokens(false); // 是否重用刷新令牌
    }

    /**
     * 令牌存储 - 使用 Redis
     */
    @Bean
    public TokenStore tokenStore() {
        // 使用 Redis 存储令牌
        return new RedisTokenStore(redisConnectionFactory);
        // 使用 JWT 存储令牌
        // return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 客户端详情服务 - 使用 JDBC
     */
    @Bean
    public ClientDetailsService jdbcClientDetailsService() {
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        clientDetailsService.setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }

    /**
     * JWT 令牌转换器
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 设置 JWT 签名密钥
        converter.setSigningKey("my-signing-key");
        return converter;
    }

    /**
     * JWT 令牌增强器
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new CustomTokenEnhancer();
    }
}
7.2 自定义令牌增强器
package com.example.oauth2.config;
import com.example.oauth2.entity.SysUser;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import java.util.HashMap;
import java.util.Map;

public class CustomTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Map<String, Object> additionalInfo = new HashMap<>();
        // 添加自定义信息到令牌中
        Object principal = authentication.getUserAuthentication().getPrincipal();
        if (principal instanceof SysUser) {
            SysUser user = (SysUser) principal;
            additionalInfo.put("user_id", user.getId());
            additionalInfo.put("username", user.getUsername());
            additionalInfo.put("email", user.getEmail());
        } else if (principal instanceof User) {
            User user = (User) principal;
            additionalInfo.put("username", user.getUsername());
        }
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return accessToken;
    }
}

8. OAuth2 资源服务器配置

8.1 资源服务器配置
package com.example.oauth2.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import javax.annotation.Resource;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    private static final String RESOURCE_ID = "resource-server";

    @Resource
    private TokenStore tokenStore;

    /**
     * 配置资源 ID 和令牌服务
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources
            .resourceId(RESOURCE_ID)
            .tokenStore(tokenStore)
            .stateless(true); // 无状态模式
    }

    /**
     * 配置资源权限规则
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/api/public/**").permitAll() // 公开接口
            .antMatchers("/api/admin/**").hasRole("ADMIN") // 需要 ADMIN 角色
            .antMatchers("/api/user/**").hasRole("USER") // 需要 USER 角色
            .anyRequest().authenticated() // 其他接口需要认证
            .and()
            .csrf().disable();
    }
}

9. JWT 令牌配置

9.1 JWT 工具类
package com.example.oauth2.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Slf4j
@Component
public class JwtTokenUtil {
    @Value("${jwt.secret:mySecretKey}")
    private String secret;

    @Value("${jwt.expiration:86400}")
    private Long expiration;

    /**
     * 从令牌中获取用户名
     */
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    /**
     * 从令牌中获取过期时间
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    /**
     * 从令牌中获取声明
     */
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    /**
     * 从令牌中获取所有声明
     */
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
    }

    /**
     * 检查令牌是否过期
     */
    private Boolean isTokenExpired(String token) {
        Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * 生成令牌
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }

    /**
     * 生成令牌
     */
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        Date createdDate = new Date();
        Date expirationDate = new Date(createdDate.getTime() + expiration * 1000);
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(subject)
            .setIssuedAt(createdDate)
            .setExpiration(expirationDate)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }

    /**
     * 验证令牌
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    /**
     * 刷新令牌
     */
    public String refreshToken(String token) {
        String username = getUsernameFromToken(token);
        return generateToken(new org.springframework.security.core.userdetails.User(username, "", java.util.Collections.emptyList()));
    }
}

10. 控制器开发

10.1 用户控制器
package com.example.oauth2.controller;
import com.example.oauth2.entity.SysUser;
import com.example.oauth2.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {
    @Autowired
    private UserService userService;

    /**
     * 获取当前用户信息
     */
    @GetMapping("/me")
    public ResponseEntity<SysUser> getCurrentUser() {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        SysUser user = userService.findByUsername(username);
        return ResponseEntity.ok(user);
    }

    /**
     * 获取所有用户(需要 ADMIN 权限)
     */
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/list")
    public ResponseEntity<List<SysUser>> getAllUsers() {
        List<SysUser> users = userService.findAll();
        return ResponseEntity.ok(users);
    }

    /**
     * 根据 ID 获取用户
     */
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/{id}")
    public ResponseEntity<SysUser> getUserById(@PathVariable Long id) {
        SysUser user = userService.findById(id);
        return ResponseEntity.ok(user);
    }

    /**
     * 创建用户
     */
    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping
    public ResponseEntity<SysUser> createUser(@RequestBody SysUser user) {
        SysUser createdUser = userService.createUser(user);
        return ResponseEntity.ok(createdUser);
    }

    /**
     * 更新用户
     */
    @PreAuthorize("hasRole('ADMIN')")
    @PutMapping("/{id}")
    public ResponseEntity<SysUser> updateUser(@PathVariable Long id, @RequestBody SysUser user) {
        user.setId(id);
        SysUser updatedUser = userService.updateUser(user);
        return ResponseEntity.ok(updatedUser);
    }

    /**
     * 删除用户
     */
    @PreAuthorize("hasRole('ADMIN')")
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.ok().build();
    }
}
10.2 公开接口控制器
package com.example.oauth2.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/public")
public class PublicController {
    @GetMapping("/info")
    public Map<String, String> getPublicInfo() {
        Map<String, String> result = new HashMap<>();
        result.put("message", "这是一个公开接口,无需认证即可访问");
        result.put("timestamp", String.valueOf(System.currentTimeMillis()));
        return result;
    }

    @GetMapping("/health")
    public Map<String, String> healthCheck() {
        Map<String, String> result = new HashMap<>();
        result.put("status", "UP");
        result.put("service", "oauth2-service");
        return result;
    }
}

11. 服务层实现

11.1 用户服务
package com.example.oauth2.service;
import com.example.oauth2.entity.SysUser;
import com.example.oauth2.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Optional;

@Slf4j
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 根据用户名查找用户
     */
    public SysUser findByUsername(String username) {
        return userRepository.findByUsernameWithRoles(username)
            .orElseThrow(() -> new RuntimeException("用户不存在:" + username));
    }

    /**
     * 根据 ID 查找用户
     */
    public SysUser findById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("用户不存在,ID: " + id));
    }

    /**
     * 查找所有用户
     */
    public List<SysUser> findAll() {
        return userRepository.findAll();
    }

    /**
     * 创建用户
     */
    @Transactional
    public SysUser createUser(SysUser user) {
        // 检查用户名是否已存在
        if (userRepository.existsByUsername(user.getUsername())) {
            throw new RuntimeException("用户名已存在:" + user.getUsername());
        }
        // 检查邮箱是否已存在
        if (user.getEmail() != null && userRepository.existsByEmail(user.getEmail())) {
            throw new RuntimeException("邮箱已存在:" + user.getEmail());
        }
        // 加密密码
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        user.setCreateTime(new Date());
        user.setUpdateTime(new Date());
        return userRepository.save(user);
    }

    /**
     * 更新用户
     */
    @Transactional
    public SysUser updateUser(SysUser user) {
        Optional<SysUser> existingUser = userRepository.findById(user.getId());
        if (!existingUser.isPresent()) {
            throw new RuntimeException("用户不存在,ID: " + user.getId());
        }
        SysUser userToUpdate = existingUser.get();
        // 更新字段
        if (user.getEmail() != null) {
            userToUpdate.setEmail(user.getEmail());
        }
        if (user.getPhone() != null) {
            userToUpdate.setPhone(user.getPhone());
        }
        if (user.getEnabled() != null) {
            userToUpdate.setEnabled(user.getEnabled());
        }
        if (user.getRoles() != null) {
            userToUpdate.setRoles(user.getRoles());
        }
        userToUpdate.setUpdateTime(new Date());
        return userRepository.save(userToUpdate);
    }

    /**
     * 删除用户
     */
    @Transactional
    public void deleteUser(Long id) {
        if (!userRepository.existsById(id)) {
            throw new RuntimeException("用户不存在,ID: " + id);
        }
        userRepository.deleteById(id);
    }

    /**
     * 修改密码
     */
    @Transactional
    public void changePassword(String username, String oldPassword, String newPassword) {
        SysUser user = findByUsername(username);
        // 验证旧密码
        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
            throw new RuntimeException("旧密码错误");
        }
        // 更新密码
        user.setPassword(passwordEncoder.encode(newPassword));
        user.setUpdateTime(new Date());
        userRepository.save(user);
        log.info("用户 {} 修改密码成功", username);
    }
}

12. 全局异常处理

12.1 全局异常处理器
package com.example.oauth2.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 处理认证异常
     */
    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<Map<String, Object>> handleAuthenticationException(AuthenticationException e) {
        log.error("认证异常:{}", e.getMessage());
        Map<String, Object> result = new HashMap<>();
        result.put("code", HttpStatus.UNAUTHORIZED.value());
        result.put("message", "认证失败:" + e.getMessage());
        result.put("timestamp", System.currentTimeMillis());
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(result);
    }

    /**
     * 处理权限不足异常
     */
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<Map<String, Object>> handleAccessDeniedException(AccessDeniedException e) {
        log.error("权限不足:{}", e.getMessage());
        Map<String, Object> result = new HashMap<>();
        result.put("code", HttpStatus.FORBIDDEN.value());
        result.put("message", "权限不足:" + e.getMessage());
        result.put("timestamp", System.currentTimeMillis());
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(result);
    }

    /**
     * 处理业务异常
     */
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException e) {
        log.error("业务异常:{}", e.getMessage());
        Map<String, Object> result = new HashMap<>();
        result.put("code", HttpStatus.BAD_REQUEST.value());
        result.put("message", e.getMessage());
        result.put("timestamp", System.currentTimeMillis());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
    }

    /**
     * 处理其他异常
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleException(Exception e) {
        log.error("系统异常:{}", e.getMessage(), e);
        Map<String, Object> result = new HashMap<>();
        result.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        result.put("message", "系统内部错误");
        result.put("timestamp", System.currentTimeMillis());
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
    }
}

13. 配置文件

13.1 application.yml
server:
  port: 8080
  servlet:
    context-path: /
spring:
  application:
    name: spring-security-oauth2-demo
  # 数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/oauth2_demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: password
  # JPA 配置
  jpa:
    hibernate:
      ddl-auto: update
      show-sql: true
      properties:
        hibernate:
          dialect: org.hibernate.dialect.MySQL5InnoDBDialect
          format_sql: true
  # Redis 配置
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0
        timeout: 3000ms
  # 日志配置
  logging:
    level:
      com.example.oauth2: DEBUG
      org.springframework.security: DEBUG
      org.springframework.security.oauth2: DEBUG
# JWT 配置
jwt:
  secret: mySecretKey
  expiration: 86400
# 安全配置
security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
    authorization:
      check-token-access: permitAll()

14. 测试数据初始化

14.1 数据初始化脚本
package com.example.oauth2.config;
import com.example.oauth2.entity.SysRole;
import com.example.oauth2.entity.SysUser;
import com.example.oauth2.repository.RoleRepository;
import com.example.oauth2.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Date;

@Slf4j
@Component
public class DataInitializer implements CommandLineRunner {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private RoleRepository roleRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void run(String... args) throws Exception {
        // 初始化角色
        if (roleRepository.count() == 0) {
            SysRole adminRole = new SysRole();
            adminRole.setRoleName("管理员");
            adminRole.setRoleCode("ROLE_ADMIN");
            adminRole.setDescription("系统管理员");
            adminRole.setCreateTime(new Date());

            SysRole userRole = new SysRole();
            userRole.setRoleName("普通用户");
            userRole.setRoleCode("ROLE_USER");
            userRole.setDescription("普通用户");
            userRole.setCreateTime(new Date());

            roleRepository.saveAll(Arrays.asList(adminRole, userRole));
            log.info("初始化角色数据完成");
        }

        // 初始化管理员用户
        if (userRepository.count() == 0) {
            SysRole adminRole = roleRepository.findByRoleCode("ROLE_ADMIN")
                .orElseThrow(() -> new RuntimeException("管理员角色不存在"));

            SysUser adminUser = new SysUser();
            adminUser.setUsername("admin");
            adminUser.setPassword(passwordEncoder.encode("admin123"));
            adminUser.setEmail("[email protected]");
            adminUser.setPhone("13800000000");
            adminUser.setRoles(Arrays.asList(adminRole));
            adminUser.setCreateTime(new Date());
            adminUser.setUpdateTime(new Date());

            userRepository.save(adminUser);
            log.info("初始化管理员用户完成,用户名:admin, 密码:admin123");
        }

        // 初始化 OAuth2 客户端
        // 这里可以通过 SQL 脚本初始化,也可以在启动后通过管理界面添加
    }
}

15. 测试与验证

15.1 获取访问令牌

使用密码模式获取访问令牌:

# 获取访问令牌
curl -X POST \
  http://localhost:8080/oauth/token \
  -H 'Authorization: Basic Y2xpZW50OnNlY3JldA==' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=password&username=admin&password=admin123&scope=all'

响应示例:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_in": 3599,
  "scope": "all",
  "user_id": 1,
  "username": "admin",
  "email": "[email protected]"
}
15.2 访问受保护资源
# 访问当前用户信息
curl -X GET \
  http://localhost:8080/api/user/me \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

# 访问用户列表(需要 ADMIN 权限)
curl -X GET \
  http://localhost:8080/api/user/list \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

# 访问公开接口(无需认证)
curl -X GET http://localhost:8080/api/public/info

16. 安全最佳实践

16.1 安全配置建议
  1. 使用 HTTPS:在生产环境中始终使用 HTTPS
  2. 强密码策略:实施密码复杂度要求
  3. 令牌过期时间:设置合理的访问令牌和刷新令牌过期时间
  4. 限制重试次数:防止暴力破解
  5. 定期更换密钥:定期更换 JWT 签名密钥
16.2 监控与日志
  1. 记录认证日志:记录所有认证成功和失败事件
  2. 监控异常行为:监控异常登录模式
  3. 定期审计:定期审计权限分配和令牌使用情况

总结

本文详细介绍了 Spring Boot 2.0 整合 Spring Security OAuth2 的完整流程,包括:

  1. 理论基础:OAuth2 的核心概念和授权模式
  2. 环境搭建:项目创建和依赖配置
  3. 数据库设计:用户、角色和 OAuth2 相关表结构
  4. 实体类设计:JPA 实体和 Spring Security 集成
  5. 安全配置:Spring Security 和 OAuth2 服务器配置
  6. JWT 集成:JWT 令牌的生成和验证
  7. API 开发:受保护和公开接口的实现
  8. 异常处理:全局异常处理机制
  9. 测试验证:完整的测试流程

目录

  1. 1. OAuth2 基础概念与原理
  2. 1.1 OAuth2 是什么
  3. 1.2 OAuth2 核心角色
  4. 1.3 OAuth2 授权模式
  5. 2. 环境准备与项目搭建
  6. 2.1 创建 Spring Boot 项目
  7. 2.2 Maven 依赖配置
  8. 3. 数据库设计
  9. 3.1 用户表结构
  10. 3.2 角色表结构
  11. 3.3 用户角色关联表
  12. 3.4 OAuth2 客户端表
  13. 4. 实体类设计
  14. 4.1 用户实体
  15. 4.2 角色实体
  16. 5. 数据访问层
  17. 5.1 用户 Repository
  18. 5.2 角色 Repository
  19. 6. 安全配置
  20. 6.1 Spring Security 配置
  21. 6.2 用户详情服务
  22. 7. OAuth2 授权服务器配置
  23. 7.1 授权服务器配置
  24. 7.2 自定义令牌增强器
  25. 8. OAuth2 资源服务器配置
  26. 8.1 资源服务器配置
  27. 9. JWT 令牌配置
  28. 9.1 JWT 工具类
  29. 10. 控制器开发
  30. 10.1 用户控制器
  31. 10.2 公开接口控制器
  32. 11. 服务层实现
  33. 11.1 用户服务
  34. 12. 全局异常处理
  35. 12.1 全局异常处理器
  36. 13. 配置文件
  37. 13.1 application.yml
  38. 数据源配置
  39. JPA 配置
  40. Redis 配置
  41. 日志配置
  42. JWT 配置
  43. 安全配置
  44. 14. 测试数据初始化
  45. 14.1 数据初始化脚本
  46. 15. 测试与验证
  47. 15.1 获取访问令牌
  48. 获取访问令牌
  49. 15.2 访问受保护资源
  50. 访问当前用户信息
  51. 访问用户列表(需要 ADMIN 权限)
  52. 访问公开接口(无需认证)
  53. 16. 安全最佳实践
  54. 16.1 安全配置建议
  55. 16.2 监控与日志
  56. 总结

更多推荐文章

查看全部
  • Python 办公自动化实战:Excel、Word 与 PPT 批量处理指南
  • AI 辅助游戏开发:基于 DeepSeek 构建贪吃蛇游戏
  • Python 概率分析:为什么葫芦娃救爷爷一个一个救成功率最高
  • Android 架构演进:MVC、MVP 与 MVVM 深度解析
  • 基于 LangChain 实现数据库问答机器人
  • Visual Studio 2026 Insider C++ 项目属性推荐配置
  • Python 基础语法
  • 策略模式实战:通过组合实现算法的灵活解耦
  • OpenClaw 云端服务器通过 SSH 控制本地 Windows 浏览器配置指南
  • RocketMQ 5.1.4 部署、Docker Dashboard 可视化及 JDK 21 安装指南
  • 2026 年 AI 辅助编程工具对比:Copilot、Cursor、Claude Code 与 Codex
  • Web Bluetooth API 实战指南:从设备连接到数据交互
  • 大模型面试常见问题与核心解析
  • 基于 NVIDIA DGX Spark 部署 Stable Diffusion 3.5 及 ComfyUI
  • Hive 常用聚合函数 collect_list 详解
  • 告别 SQL 恐惧症:用自然语言轻松搞定数据库查询
  • 大模型 AI 产品经理学习路线:从基础到精通
  • 2026 年 3 月 GESP C++ 一级真题及解析:数字替换
  • 网络安全入门核心知识体系与学习路径指南
  • VS Code 远程配置 GitHub Copilot 无法使用的排查与避坑指南

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online