【Springboot相关知识】Springboot结合SpringSecurity实现身份认证以及接口鉴权
Springboot结合SpringSecurity实现身份认证以及接口鉴权
- 身份认证
- 1. 添加依赖
- 2. 配置JWT工具类
- 3. 配置Spring Security
- 4. 创建JWT请求过滤器
- 5. 创建认证控制器
- 6. 创建请求和响应对象
- 7. 配置UserDetailsService
- 8. 运行应用程序
- 9. 测试
- 总结
- 接口鉴权
- 1. 启用方法级安全注解
- 2. 定义角色和权限
- 示例:定义用户角色
- 3. 使用注解进行接口鉴权
- 1. `@PreAuthorize`
- 2. `@PostAuthorize`
- 3. `@Secured`
- 4. `@RolesAllowed`
- 4. 配置全局异常处理
- 5. 测试接口鉴权
- 1. 登录获取Token
- 2. 访问受保护的接口
- 6. 总结
- 相关文献
身份认证
在Spring Boot中,使用Spring Security和JWT(JSON Web Token)实现身份认证和接口鉴权是一个常见的需求。下面是一个完整的示例,展示了如何实现这些功能,并支持Token刷新。
1. 添加依赖
首先,在pom.xml
中添加必要的依赖:
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT Library -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Spring Boot Starter Data JPA (Optional, for UserDetailsService) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 Database (Optional, for testing) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
2. 配置JWT工具类
创建一个JWT工具类,用于生成和解析JWT Token。
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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;
@Component
public class JwtUtil {
private String SECRET_KEY = "secret";
// 生成Token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
// 创建Token
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10小时有效期
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 验证Token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 提取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 提取过期时间
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// 提取Claim
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// 提取所有Claim
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
// 判断Token是否过期
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}
3. 配置Spring Security
创建一个Spring Security配置类,配置身份认证和接口鉴权。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/authenticate", "/refreshToken").permitAll() // 允许匿名访问的接口
.anyRequest().authenticated() // 其他接口需要认证
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 无状态会话
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // 仅用于示例,生产环境应使用BCryptPasswordEncoder
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
4. 创建JWT请求过滤器
创建一个过滤器,用于在每次请求中验证JWT Token。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
5. 创建认证控制器
创建一个控制器,用于处理用户登录和Token刷新。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@RestController
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
} catch (Exception e) {
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(jwt));
}
@PostMapping("/refreshToken")
public ResponseEntity<?> refreshAuthenticationToken(@RequestHeader("Authorization") String oldToken) {
String username = jwtUtil.extractUsername(oldToken.substring(7));
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(oldToken.substring(7), userDetails)) {
String newToken = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(newToken));
}
return ResponseEntity.badRequest().body("Invalid token");
}
}
6. 创建请求和响应对象
创建用于封装请求和响应的对象。
public class AuthenticationRequest {
private String username;
private String password;
// Getters and Setters
}
public class AuthenticationResponse {
private final String jwt;
public AuthenticationResponse(String jwt) {
this.jwt = jwt;
}
public String getJwt() {
return jwt;
}
}
7. 配置UserDetailsService
实现UserDetailsService
接口,用于加载用户信息。
import org.springframework.security.core.userdetails.User;
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;
import java.util.ArrayList;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 这里可以从数据库中加载用户信息
return new User("user", "password", new ArrayList<>());
}
}
8. 运行应用程序
现在,你可以运行Spring Boot应用程序,并通过以下接口进行测试:
- 登录接口:
POST /authenticate
- 刷新Token接口:
POST /refreshToken
- 受保护的接口: 任何其他接口都需要在请求头中添加
Authorization: Bearer <token>
。
9. 测试
你可以使用Postman或curl等工具进行测试:
-
登录请求:
POST /authenticate { "username": "user", "password": "password" }
-
刷新Token请求:
POST /refreshToken Authorization: Bearer <old_token>
总结
通过以上步骤,我们实现了一个基于Spring Security和JWT的身份认证和接口鉴权系统,并支持Token刷新。你可以根据实际需求进一步扩展和优化这个系统。
接口鉴权
在Spring Security中,接口鉴权通常通过配置HttpSecurity
来实现。我们可以使用注解(如@PreAuthorize
、@PostAuthorize
、@Secured
等)来细粒度地控制接口的访问权限。以下是接口鉴权的详细实现和示例。
1. 启用方法级安全注解
在Spring Boot中,默认情况下方法级安全注解是关闭的。我们需要在配置类中启用它。
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true, // 启用@PreAuthorize和@PostAuthorize注解
securedEnabled = true, // 启用@Secured注解
jsr250Enabled = true // 启用@RolesAllowed注解
)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}
2. 定义角色和权限
在Spring Security中,角色和权限通常通过UserDetails
实现类来定义。我们可以为用户分配角色或权限,然后在接口上使用注解进行鉴权。
示例:定义用户角色
在UserDetailsService
中,为用户分配角色:
import org.springframework.security.core.userdetails.User;
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;
import java.util.Arrays;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("admin".equals(username)) {
return new User("admin", "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
} else if ("user".equals(username)) {
return new User("user", "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}
3. 使用注解进行接口鉴权
Spring Security提供了多种注解来实现接口鉴权。以下是常用的注解及其用法:
1. @PreAuthorize
在方法执行前进行权限检查。
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiController {
// 只有ADMIN角色可以访问
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminEndpoint() {
return "Hello Admin!";
}
// 只有USER角色可以访问
@GetMapping("/user")
@PreAuthorize("hasRole('USER')")
public String userEndpoint() {
return "Hello User!";
}
// 同时支持ADMIN和USER角色
@GetMapping("/all")
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
public String allEndpoint() {
return "Hello All!";
}
// 自定义权限表达式
@GetMapping("/custom")
@PreAuthorize("hasAuthority('READ_PRIVILEGE')")
public String customEndpoint() {
return "Custom Access!";
}
}
2. @PostAuthorize
在方法执行后进行权限检查。
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiController {
// 方法执行后检查返回值
@GetMapping("/postAuth")
@PostAuthorize("returnObject == 'Hello Admin!'")
public String postAuthEndpoint() {
return "Hello Admin!";
}
}
3. @Secured
基于角色的简单鉴权。
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiController {
// 只有ADMIN角色可以访问
@GetMapping("/secured")
@Secured("ROLE_ADMIN")
public String securedEndpoint() {
return "Secured Access!";
}
}
4. @RolesAllowed
基于JSR-250标准的角色鉴权。
import javax.annotation.security.RolesAllowed;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiController {
// 只有ADMIN角色可以访问
@GetMapping("/rolesAllowed")
@RolesAllowed("ADMIN")
public String rolesAllowedEndpoint() {
return "Roles Allowed Access!";
}
}
4. 配置全局异常处理
当用户访问未授权的接口时,Spring Security会抛出AccessDeniedException
。我们可以通过全局异常处理来返回友好的错误信息。
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理权限不足异常
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<String> handleAccessDeniedException(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access Denied: " + e.getMessage());
}
// 处理其他异常
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error: " + e.getMessage());
}
}
5. 测试接口鉴权
使用Postman或curl测试接口:
1. 登录获取Token
POST /authenticate
{
"username": "admin",
"password": "password"
}
2. 访问受保护的接口
-
ADMIN接口:
GET /api/admin Authorization: Bearer <token>
-
USER接口:
GET /api/user Authorization: Bearer <token>
-
未授权访问:
如果用户没有权限,会返回403 Forbidden
。
6. 总结
通过以上步骤,我们实现了基于Spring Security的接口鉴权功能。主要步骤包括:
- 启用方法级安全注解。
- 定义用户角色和权限。
- 使用注解(如
@PreAuthorize
、@Secured
等)控制接口访问权限。 - 配置全局异常处理,返回友好的错误信息。
你可以根据实际需求扩展和调整这些配置,例如从数据库中动态加载权限、支持更复杂的权限表达式等。
相关文献
JWT(JSON Web Tokens) 详细介绍
【spring知识】Spring Security从入门到放弃