下面是一个完整的基于Spring Security OAuth2的单点登录系统实现,满足您提出的要求:

  1. 使用数据库存储用户账号密码等信息
  1. 使用Redis存储会话信息提高性能
  1. 采用JWT实现令牌(访问令牌+刷新令牌)

一、系统架构

graph TD
    A[客户端应用] --> B[认证服务器]
    C[客户端应用] --> B
    D[资源服务器] --> B
    B --> E[(MySQL数据库)]
    B --> F[(Redis缓存)]
    D --> F

二、实现代码

1. 数据库设计 (MySQL)

-- 用户表
CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    mobile VARCHAR(20) UNIQUE,
    email VARCHAR(100),
    enabled BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 角色表
CREATE TABLE roles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE
);

-- 用户角色关联表
CREATE TABLE user_roles (
    user_id BIGINT NOT NULL,
    role_id BIGINT NOT NULL,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (role_id) REFERENCES roles(id)
);

-- OAuth2客户端表
CREATE TABLE oauth_client_details (
    client_id VARCHAR(255) PRIMARY KEY,
    resource_ids VARCHAR(255),
    client_secret VARCHAR(255) NOT NULL,
    scope VARCHAR(255),
    authorized_grant_types VARCHAR(255),
    web_server_redirect_uri VARCHAR(255),
    authorities VARCHAR(255),
    access_token_validity INTEGER,
    refresh_token_validity INTEGER,
    additional_information TEXT,
    autoapprove VARCHAR(255)
);

2. 认证服务器实现

主应用类

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

安全配置类

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/oauth/**", "/login**", "/logout**").permitAll()
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
            .and()
            .logout()
                .permitAll();
    }
}

用户详情服务实现

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库加载用户信息
        String sql = "SELECT u.id, u.username, u.password, u.enabled, r.name as role " +
                     "FROM users u " +
                     "LEFT JOIN user_roles ur ON u.id = ur.user_id " +
                     "LEFT JOIN roles r ON ur.role_id = r.id " +
                     "WHERE u.username = ? OR u.mobile = ?";
        
        List<UserDetails> users = jdbcTemplate.query(sql, new Object[]{username, username},
            (rs, rowNum) -> {
                boolean enabled = rs.getBoolean("enabled");
                String password = rs.getString("password");
                String userAuthorities = rs.getString("role");
                
                return User.builder()
                    .username(rs.getString("username"))
                    .password(password)
                    .disabled(!enabled)
                    .authorities(getAuthorities(userAuthorities))
                    .build();
            });
        
        if (users.isEmpty()) {
            throw new UsernameNotFoundException("用户不存在");
        }
        
        return users.get(0);
    }
    
    private Collection<? extends GrantedAuthority> getAuthorities(String role) {
        return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role));
    }
}

授权服务器配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private DataSource dataSource;
    
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    
    @Value("${security.oauth2.jwt.signing-key}")
    private String signingKey;
    
    @Bean
    public TokenStore tokenStore() {
        // 使用Redis存储令牌
        return new RedisTokenStore(redisConnectionFactory);
    }
    
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(signingKey);
        return converter;
    }
    
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new CustomTokenEnhancer();
    }
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
        
        endpoints
            .tokenStore(tokenStore())
            .tokenEnhancer(tokenEnhancerChain)
            .authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService)
            .reuseRefreshTokens(false); // 每次刷新都生成新的刷新令牌
    }
    
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
            .tokenKeyAccess("permitAll()")
            .checkTokenAccess("isAuthenticated()")
            .allowFormAuthenticationForClients();
    }
}

自定义令牌增强器

public class CustomTokenEnhancer extends JwtAccessTokenConverter {
    
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        // 添加额外信息到JWT负载中
        Map<String, Object> additionalInfo = new HashMap<>();
        additionalInfo.put("organization", "MyCompany");
        
        if (authentication.getUserAuthentication() != null) {
            Object principal = authentication.getUserAuthentication().getPrincipal();
            if (principal instanceof UserDetails) {
                UserDetails userDetails = (UserDetails) principal;
                additionalInfo.put("user_id", userDetails.getUsername());
                additionalInfo.put("roles", userDetails.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toList()));
            }
        }
        
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return super.enhance(accessToken, authentication);
    }
}

3. 资源服务器实现

资源服务器配置

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    @Autowired
    private TokenStore tokenStore;
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.tokenStore(tokenStore).resourceId("resource_id");
    }
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/private/**").authenticated()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
            .and()
            .requestMatchers()
                .antMatchers("/api/**");
    }
}

控制器示例

@RestController
@RequestMapping("/api")
public class ApiController {
    
    @GetMapping("/public/hello")
    public ResponseEntity<?> publicHello() {
        return ResponseEntity.ok("Public Hello World!");
    }
    
    @GetMapping("/private/hello")
    public ResponseEntity<?> privateHello(@AuthenticationPrincipal Jwt jwt) {
        Map<String, Object> claims = jwt.getClaims();
        return ResponseEntity.ok("Private Hello, " + claims.get("user_id"));
    }
    
    @GetMapping("/admin/dashboard")
    public ResponseEntity<?> adminDashboard() {
        return ResponseEntity.ok("Admin Dashboard");
    }
}

4. 客户端应用实现

安全配置

@Configuration
@EnableWebSecurity
public class ClientSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/login**").permitAll()
                .anyRequest().authenticated()
            .and()
            .oauth2Login()
                .defaultSuccessUrl("/dashboard")
                .failureUrl("/login?error=true")
            .and()
            .logout()
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID");
    }
}

控制器示例

@Controller
public class ClientController {
    
    @GetMapping("/")
    public String home() {
        return "index";
    }
    
    @GetMapping("/dashboard")
    public String dashboard(Model model, 
                           @RegisteredOAuth2AuthorizedClient("my-auth") 
                           OAuth2AuthorizedClient authorizedClient,
                           @AuthenticationPrincipal OidcUser principal) {
        
        model.addAttribute("username", principal.getName());
        model.addAttribute("email", principal.getEmail());
        
        // 获取访问令牌
        String accessToken = authorizedClient.getAccessToken().getTokenValue();
        model.addAttribute("accessToken", accessToken);
        
        return "dashboard";
    }
}

5. Redis配置

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisConnectionFactory redisConnectionFactory(
            @Value("${spring.redis.host}") String host,
            @Value("${spring.redis.port}") int port) {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
        return new LettuceConnectionFactory(config);
    }
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

6. 配置文件

认证服务器配置 (application.yml)

server:
  port: 8080

spring:
  application:
    name: auth-server
  datasource:
    url: jdbc:mysql://localhost:3306/oauth2_db?useSSL=false&characterEncoding=utf8
    username: root
    password: root123
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: localhost
    port: 6379
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

security:
  oauth2:
    jwt:
      signing-key: mySecretKeyForJWTSigning # 生产环境应使用强密钥
    authorization:
      client:
        client-id: my-client
        client-secret: my-secret
        scope: read,write
        authorized-grant-types: authorization_code,refresh_token
        redirect-uris: http://localhost:8081/login/oauth2/code/my-auth,http://localhost:8082/login/oauth2/code/my-auth

资源服务器配置 (application.yml)

server:
  port: 9000

spring:
  application:
    name: resource-server
  redis:
    host: localhost
    port: 6379

security:
  oauth2:
    resource:
      jwt:
        key-value: mySecretKeyForJWTSigning # 必须与认证服务器相同
      id: resource_id

客户端应用配置 (application.yml)

server:
  port: 8081

spring:
  application:
    name: client-app
  security:
    oauth2:
      client:
        registration:
          my-auth:
            client-id: my-client
            client-secret: my-secret
            scope: read,write
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8081/login/oauth2/code/my-auth
        provider:
          my-auth:
            authorization-uri: http://localhost:8080/oauth/authorize
            token-uri: http://localhost:8080/oauth/token
            user-info-uri: http://localhost:8080/userinfo
            user-name-attribute: sub

三、工作流程

sequenceDiagram
    participant User as 用户
    participant Client as 客户端应用
    participant AuthServer as 认证服务器
    participant ResourceServer as 资源服务器
    participant DB as MySQL数据库
    participant Redis as Redis缓存
    
    User->>Client: 访问受保护资源
    Client->>User: 重定向到登录页
    User->>AuthServer: 提交凭证(用户名/密码)
    AuthServer->>DB: 验证用户凭证
    DB-->>AuthServer: 返回用户信息
    AuthServer->>Redis: 存储会话信息
    AuthServer->>AuthServer: 生成JWT令牌(访问+刷新)
    AuthServer->>Client: 重定向回客户端(带授权码)
    Client->>AuthServer: 使用授权码交换令牌
    AuthServer-->>Client: 返回访问令牌和刷新令牌
    Client->>ResourceServer: 请求资源(带访问令牌)
    ResourceServer->>ResourceServer: 验证JWT令牌
    ResourceServer-->>Client: 返回受保护资源
    Client->>User: 显示资源内容
    
    Note over Client,ResourceServer: 访问令牌过期后
    Client->>AuthServer: 使用刷新令牌获取新访问令牌
    AuthServer->>Redis: 验证刷新令牌
    AuthServer-->>Client: 返回新的访问令牌

四、关键技术

  1. 数据库存储
    • 使用MySQL存储用户账号、密码、手机号等信息
    • 使用JDBC存储OAuth2客户端配置
  1. Redis缓存
    • 存储会话信息提高性能
    • 存储OAuth2令牌(访问令牌和刷新令牌)
    • 配置Redis连接工厂和模板
  1. JWT令牌实现
    • 访问令牌:短期有效(如30分钟),包含用户信息和权限
    • 刷新令牌:长期有效(如7天),用于获取新的访问令牌
    • 使用RSA非对称加密保证安全性(生产环境推荐)
  1. 令牌增强
    • 在JWT中添加自定义声明(如组织信息、用户ID等)
    • 包含用户角色和权限信息
  1. 单点登录流程
    • 用户在一个客户端登录后,认证服务器创建会话
    • 用户访问其他客户端时,认证服务器检测到已有会话
    • 直接发放授权码,无需重新登录

五、安全建议

  1. 密钥管理
    • 使用非对称密钥对(RSA)代替对称密钥
    • 将私钥存储在安全的密钥管理系统(如Vault)
  1. 令牌生命周期
    • 访问令牌:短时间有效(15-30分钟)
    • 刷新令牌:较长时间有效(7天),可撤销
  1. 令牌撤销
    • 实现令牌撤销端点
    • 将撤销的令牌存储在Redis黑名单中
  1. 限流与防护
    • 在认证服务器添加速率限制
    • 防止暴力破解攻击
  1. 审计日志
    • 记录所有认证和授权事件
    • 监控异常登录行为

六、部署架构

+-----------------+     +-----------------+     +-----------------+
|  客户端应用1      |     |  客户端应用2      |     |  资源服务器      |
| (Port: 8081)    |     | (Port: 8082)    |     | (Port: 9000)    |
+--------+--------+     +--------+--------+     +--------+--------+
         |                      |                       |
         |                      |                       |
         |          +-----------v-----------+           |
         |          |     认证服务器         |           |
         |          | (Authorization Server)|           |
         |          | (Port: 8080)          |           |
         |          +-----------+-----------+           |
         |                      |                       |
         |                      |                       |
+--------v----------------------v-----------------------v--------+
|                         共享存储                              |
|  +----------------+     +----------------+     +-----------+ |
|  |   MySQL数据库   |     |    Redis缓存    |     | 日志系统  | |
|  | (用户数据/OAuth2 |     | (会话/令牌存储) |     | (ELK)    | |
|  |   客户端配置)    |     +----------------+     +-----------+ |
|  +----------------+                                        |
+-------------------------------------------------------------+

这个实现提供了一个完整的基于Spring Security OAuth2的单点登录系统,满足您的所有要求,并包含了生产环境所需的安全性和可扩展性考虑。