Spring Boot 2.0 整合 Spring Security OAuth2的完整实现方案
1. OAuth2 基础概念与原理
1.1 OAuth2 是什么
OAuth 2.0(开放授权 2.0)是一个行业标准的授权协议,它允许用户在不将用户名和密码提供给第三方应用的情况下,授权第三方应用访问用户存储在服务提供方的资源。
1.2 OAuth2 核心角色
- 资源所有者 (Resource Owner):能够授权访问受保护资源的实体,通常是最终用户
- 客户端 (Client):请求访问受保护资源的应用程序
- 授权服务器 (Authorization Server):在认证资源所有者并获取授权后,向客户端颁发访问令牌的服务器
- 资源服务器 (Resource Server):托管受保护资源的服务器,能够接受和响应使用访问令牌的受保护资源请求
1.3 OAuth2 授权模式
- 授权码模式 (Authorization Code):最安全、最常用的模式,适用于有后端的 Web 应用
- 简化模式 (Implicit):适用于纯前端 SPA 应用
- 密码模式 (Resource Owner Password Credentials):适用于信任的客户端,如官方应用
- 客户端模式 (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("admin@example.com");
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": "admin@example.com"
}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 安全配置建议
- 使用HTTPS:在生产环境中始终使用HTTPS
- 强密码策略:实施密码复杂度要求
- 令牌过期时间:设置合理的访问令牌和刷新令牌过期时间
- 限制重试次数:防止暴力破解
- 定期更换密钥:定期更换JWT签名密钥
16.2 监控与日志
- 记录认证日志:记录所有认证成功和失败事件
- 监控异常行为:监控异常登录模式
- 定期审计:定期审计权限分配和令牌使用情况
总结
本文详细介绍了 Spring Boot 2.0 整合 Spring Security OAuth2 的完整流程,包括:
- 理论基础:OAuth2 的核心概念和授权模式
- 环境搭建:项目创建和依赖配置
- 数据库设计:用户、角色和OAuth2相关表结构
- 实体类设计:JPA实体和Spring Security集成
- 安全配置:Spring Security和OAuth2服务器配置
- JWT集成:JWT令牌的生成和验证
- API开发:受保护和公开接口的实现
- 异常处理:全局异常处理机制
- 测试验证:完整的测试流程
到此这篇关于Spring Boot 2.0 整合 Spring Security OAuth2的文章就介绍到这了,更多相关Spring Boot 整合 Spring Security OAuth2内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
- SpringBoot的Security和OAuth2的使用示例小结
- Springboot使用Security实现OAuth2授权验证完整过程
- SpringBoot整合SpringSecurityOauth2实现鉴权动态权限问题
- springboot集成springsecurity 使用OAUTH2做权限管理的教程
- Spring security oauth2以redis作为tokenstore及jackson序列化失败问题
- SpringSecurity+OAuth2.0 搭建认证中心和资源服务中心流程分析
- SpringSecurityOAuth2实现微信授权登录
- SpringSecurity OAuth2单点登录和登出的实现
相关文章
Spring Boot 集成 Quartz 使用Cron 表达式实现定
本文介绍了如何在SpringBoot项目中集成Quartz并使用Cron表达式进行任务调度,通过添加Quartz依赖、创建Quartz任务、配置任务调度以及启动项目,可以实现定时任务的执行,Cron表达式提供了灵活的任务调度方式,适用于各种复杂的定时任务需求,感兴趣的朋友一起看看吧2025-03-03
java利用htmlparser获取html中想要的代码具体实现
这篇文章主要介绍了java利用htmlparser获取html中想要的代码具体实现,需要的朋友可以参考下2014-02-02


最新评论