跳到主要内容Spring Boot 2.0 整合 Spring Security OAuth2 | 极客日志Javajava
Spring Boot 2.0 整合 Spring Security OAuth2
综述由AI生成Spring Boot 2.0 整合 Spring Security OAuth2 的完整流程,涵盖 OAuth2 核心概念与授权模式、项目环境搭建与 Maven 依赖配置、数据库表结构设计(用户、角色、客户端)、JPA 实体与 Repository 层实现、Spring Security 安全配置、授权服务器与资源服务器配置、JWT 令牌生成与验证、RESTful API 控制器开发以及全局异常处理。文中还提供了 Redis 令牌存储方案、测试数据初始化脚本及 curl 测试命令,并总结了生产环境下的安全最佳实践。
奇形怪状14K 浏览 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>
<>spring-boot-starter-parent
2.7.0
com.example
spring-security-oauth2-demo
1.0.0
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.1.0.RELEASE
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
io.jsonwebtoken
jjwt
0.9.1
org.springframework.boot
spring-boot-maven-plugin
artifactId
</artifactId>
<version>
</version>
<relativePath/>
</parent>
<groupId>
</groupId>
<artifactId>
</artifactId>
<version>
</version>
<properties>
<java.version>
</java.version>
</properties>
<dependencies>
<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
</dependency>
<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
</dependency>
<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
<version>
</version>
</dependency>
<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
</dependency>
<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
<scope>
</scope>
</dependency>
<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
<optional>
</optional>
</dependency>
<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
<version>
</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>
</groupId>
<artifactId>
</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());
}
@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 {
clients.withClientDetails(jdbcClientDetailsService());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.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())
.reuseRefreshTokens(false);
}
@Bean
public TokenStore tokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
@Bean
public ClientDetailsService jdbcClientDetailsService() {
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
clientDetailsService.setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("my-signing-key");
return converter;
}
@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;
@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")
.antMatchers("/api/user/**").hasRole("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);
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/list")
public ResponseEntity<List<SysUser>> getAllUsers() {
List<SysUser> users = userService.findAll();
return ResponseEntity.ok(users);
}
@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));
}
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:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
format_sql: true
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:
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");
}
}
}
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...'
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 开发:受保护和公开接口的实现
- 异常处理:全局异常处理机制
- 测试验证:完整的测试流程
相关免费在线工具
- 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