背景与初衷

在现代Web应用程序中,安全性是一个至关重要的方面。随着单页应用(SPA)和微服务架构的流行,传统的基于会话的认证方式已不能完全满足需求。JSON Web Token(JWT)作为一种无状态、分布式的认证机制,得到了广泛应用。Spring Security 作为一个强大且高度可定制的安全框架,可以方便地集成JWT,实现安全高效的认证与授权。

目标

本文旨在详细介绍Spring Security中如何集成和使用JSON Web Token(JWT)。我们将探讨JWT的基本概念和工作原理,如何在Spring Security中实现JWT认证与授权,并通过详细的示例展示如何在实际应用中使用这些技术。

JWT的基本概念

什么是JWT?

JSON Web Token(JWT)是一种基于JSON的开放标准(RFC 7519),用于在各方之间传递声明信息。JWT由三部分组成:Header(头部)、Payload(负载)和Signature(签名)。

  1. Header:头部通常包含两部分信息:令牌的类型(即JWT)和使用的签名算法(如HMAC SHA256)。
  2. Payload:负载部分包含声明(claims)。声明是一些关于实体(通常是用户)和其他数据的元数据。声明有三种类型:注册声明、公共声明和私有声明。
  3. Signature:签名部分是用来验证消息在传递过程中是否被篡改。

JWT的结构

一个典型的JWT由三部分组成,每部分之间用点(.)分隔,例如:

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT的工作原理

  1. 用户认证:用户使用用户名和密码登录,服务器验证用户信息。
  2. 生成JWT:服务器生成JWT并返回给客户端。
  3. 客户端存储JWT:客户端通常将JWT存储在本地存储(如LocalStorage)或会话存储(SessionStorage)中。
  4. 客户端请求:客户端在每次请求时将JWT包含在HTTP头部中。
  5. 服务器验证JWT:服务器验证JWT的签名和有效性,然后根据JWT中的信息处理请求。

Spring Security中集成JWT

环境配置

首先,我们需要创建一个Spring Boot项目,并添加相关依赖。可以使用Spring Initializr来生成项目,并选择以下依赖项:

  • Spring Web
  • Spring Security
  • Spring Data JPA
  • H2 Database (用于示例)
  • jjwt (用于JWT生成和解析)

在生成项目后,编辑pom.xmlbuild.gradle文件,添加jjwt依赖:

  1. <dependency>
  2. <groupId>io.jsonwebtoken</groupId>
  3. <artifactId>jjwt</artifactId>
  4. <version>0.9.1</version>
  5. </dependency>

项目结构

项目的结构如下:

  1. jwt-demo
  2. ├── src
  3. ├── main
  4. ├── java
  5. ├── com
  6. ├── example
  7. ├── JwtDemoApplication.java
  8. ├── config
  9. └── SecurityConfig.java
  10. ├── controller
  11. └── AuthController.java
  12. ├── model
  13. └── User.java
  14. ├── repository
  15. └── UserRepository.java
  16. ├── security
  17. ├── JwtAuthenticationFilter.java
  18. ├── JwtAuthorizationFilter.java
  19. ├── JwtUtil.java
  20. └── UserDetailsServiceImpl.java
  21. ├── resources
  22. └── application.properties
  23. └── test
  24. └── java
  25. └── com
  26. └── example
  27. └── JwtDemoApplicationTests.java

代码实现

JwtDemoApplication.java
  1. package com.example;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. @SpringBootApplication
  5. public class JwtDemoApplication {
  6. public static void main(String[] args) {
  7. SpringApplication.run(JwtDemoApplication.class, args);
  8. }
  9. }
application.properties
  1. spring.datasource.url=jdbc:h2:mem:testdb
  2. spring.datasource.driverClassName=org.h2.Driver
  3. spring.datasource.username=sa
  4. spring.datasource.password=password
  5. spring.h2.console.enabled=true
  6. spring.jpa.hibernate.ddl-auto=update
User.java
  1. package com.example.model;
  2. import javax.persistence.Entity;
  3. import javax.persistence.GeneratedValue;
  4. import javax.persistence.GenerationType;
  5. import javax.persistence.Id;
  6. @Entity
  7. public class User {
  8. @Id
  9. @GeneratedValue(strategy = GenerationType.IDENTITY)
  10. private Long id;
  11. private String username;
  12. private String password;
  13. private String roles;
  14. // Getters and setters
  15. }
UserRepository.java
  1. package com.example.repository;
  2. import com.example.model.User;
  3. import org.springframework.data.jpa.repository.JpaRepository;
  4. public interface UserRepository extends JpaRepository<User, Long> {
  5. User findByUsername(String username);
  6. }
UserDetailsServiceImpl.java
  1. package com.example.security;
  2. import com.example.model.User;
  3. import com.example.repository.UserRepository;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.security.core.userdetails.UserDetails;
  6. import org.springframework.security.core.userdetails.UserDetailsService;
  7. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  8. import org.springframework.stereotype.Service;
  9. @Service
  10. public class UserDetailsServiceImpl implements UserDetailsService {
  11. @Autowired
  12. private UserRepository userRepository;
  13. @Override
  14. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  15. User user = userRepository.findByUsername(username);
  16. if (user == null) {
  17. throw new UsernameNotFoundException("User not found");
  18. }
  19. return org.springframework.security.core.userdetails.User
  20. .withUsername(user.getUsername())
  21. .password(user.getPassword())
  22. .roles(user.getRoles().split(","))
  23. .build();
  24. }
  25. }
JwtUtil.java
  1. package com.example.security;
  2. import io.jsonwebtoken.Claims;
  3. import io.jsonwebtoken.Jwts;
  4. import io.jsonwebtoken.SignatureAlgorithm;
  5. import org.springframework.stereotype.Component;
  6. import java.util.Date;
  7. import java.util.HashMap;
  8. import java.util.Map;
  9. import java.util.function.Function;
  10. @Component
  11. public class JwtUtil {
  12. private String SECRET_KEY = "secret";
  13. public String extractUsername(String token) {
  14. return extractClaim(token, Claims::getSubject);
  15. }
  16. public Date extractExpiration(String token) {
  17. return extractClaim(token, Claims::getExpiration);
  18. }
  19. public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
  20. final Claims claims = extractAllClaims(token);
  21. return claimsResolver.apply(claims);
  22. }
  23. private Claims extractAllClaims(String token) {
  24. return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
  25. }
  26. private Boolean isTokenExpired(String token) {
  27. return extractExpiration(token).before(new Date());
  28. }
  29. public String generateToken(String username) {
  30. Map<String, Object> claims = new HashMap<>();
  31. return createToken(claims, username);
  32. }
  33. private String createToken(Map<String, Object> claims, String subject) {
  34. return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
  35. .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
  36. .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
  37. }
  38. public Boolean validateToken(String token, UserDetails userDetails) {
  39. final String username = extractUsername(token);
  40. return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
  41. }
  42. }
JwtAuthenticationFilter.java
  1. package com.example.security;
  2. import org.springframework.security.authentication.AuthenticationManager;
  3. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  4. import org.springframework.security.core.Authentication;
  5. import org.springframework.security.core.AuthenticationException;
  6. import org.springframework.security.core.context.SecurityContextHolder;
  7. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  8. import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
  9. import javax.servlet.FilterChain;
  10. import javax.servlet.ServletException
  11. ;
  12. import javax.servlet.http.HttpServletRequest;
  13. import javax.servlet.http.HttpServletResponse;
  14. import java.io.IOException;
  15. import java.util.ArrayList;
  16. public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
  17. private AuthenticationManager authenticationManager;
  18. private JwtUtil jwtUtil;
  19. public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
  20. this.authenticationManager = authenticationManager;
  21. this.jwtUtil = jwtUtil;
  22. setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
  23. }
  24. @Override
  25. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  26. String username = request.getParameter("username");
  27. String password = request.getParameter("password");
  28. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
  29. return authenticationManager.authenticate(authRequest);
  30. }
  31. @Override
  32. protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
  33. String username = authResult.getName();
  34. String token = jwtUtil.generateToken(username);
  35. response.addHeader("Authorization", "Bearer " + token);
  36. }
  37. }
JwtAuthorizationFilter.java
  1. package com.example.security;
  2. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  3. import org.springframework.security.core.context.SecurityContextHolder;
  4. import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
  5. import org.springframework.web.filter.OncePerRequestFilter;
  6. import javax.servlet.FilterChain;
  7. import javax.servlet.ServletException;
  8. import javax.servlet.http.HttpServletRequest;
  9. import javax.servlet.http.HttpServletResponse;
  10. import java.io.IOException;
  11. public class JwtAuthorizationFilter extends OncePerRequestFilter {
  12. private UserDetailsServiceImpl userDetailsService;
  13. private JwtUtil jwtUtil;
  14. public JwtAuthorizationFilter(UserDetailsServiceImpl userDetailsService, JwtUtil jwtUtil) {
  15. this.userDetailsService = userDetailsService;
  16. this.jwtUtil = jwtUtil;
  17. }
  18. @Override
  19. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
  20. String authorizationHeader = request.getHeader("Authorization");
  21. String username = null;
  22. String jwt = null;
  23. if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
  24. jwt = authorizationHeader.substring(7);
  25. username = jwtUtil.extractUsername(jwt);
  26. }
  27. if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
  28. UserDetails userDetails = userDetailsService.loadUserByUsername(username);
  29. if (jwtUtil.validateToken(jwt, userDetails)) {
  30. UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
  31. usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
  32. SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
  33. }
  34. }
  35. chain.doFilter(request, response);
  36. }
  37. }
SecurityConfig.java
  1. package com.example.config;
  2. import com.example.security.JwtAuthenticationFilter;
  3. import com.example.security.JwtAuthorizationFilter;
  4. import com.example.security.JwtUtil;
  5. import com.example.security.UserDetailsServiceImpl;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.security.authentication.AuthenticationManager;
  10. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  11. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  12. import org.springframework.security.config.annotation.web.builders.WebSecurity;
  13. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  14. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  15. import org.springframework.security.config.http.SessionCreationPolicy;
  16. import org.springframework.security.core.userdetails.UserDetailsService;
  17. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  18. import org.springframework.security.crypto.password.PasswordEncoder;
  19. @Configuration
  20. @EnableWebSecurity
  21. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  22. @Autowired
  23. private UserDetailsServiceImpl userDetailsService;
  24. @Autowired
  25. private JwtUtil jwtUtil;
  26. @Override
  27. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  28. auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  29. }
  30. @Override
  31. @Bean
  32. public AuthenticationManager authenticationManagerBean() throws Exception {
  33. return super.authenticationManagerBean();
  34. }
  35. @Override
  36. protected void configure(HttpSecurity http) throws Exception {
  37. http.csrf().disable()
  38. .authorizeRequests().antMatchers("/login").permitAll()
  39. .anyRequest().authenticated()
  40. .and()
  41. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  42. http.addFilter(new JwtAuthenticationFilter(authenticationManagerBean(), jwtUtil));
  43. http.addFilterBefore(new JwtAuthorizationFilter(userDetailsService, jwtUtil), JwtAuthenticationFilter.class);
  44. }
  45. @Bean
  46. public PasswordEncoder passwordEncoder() {
  47. return new BCryptPasswordEncoder();
  48. }
  49. }
AuthController.java
  1. package com.example.controller;
  2. import org.springframework.web.bind.annotation.GetMapping;
  3. import org.springframework.web.bind.annotation.RestController;
  4. @RestController
  5. public class AuthController {
  6. @GetMapping("/hello")
  7. public String hello() {
  8. return "Hello, World!";
  9. }
  10. }

详细解读

在这个示例中,我们创建了一个简单的Spring Boot Web应用程序,并通过JWT实现了认证与授权。以下是关键组件的详细解读:

  1. UserDetailsServiceImpl:实现Spring Security的UserDetailsService接口,用于从数据库中加载用户信息。
  2. JwtUtil:用于生成和解析JWT,包括提取用户名、验证令牌、生成令牌等功能。
  3. JwtAuthenticationFilter:用于处理用户登录请求,验证用户名和密码,生成JWT并返回给客户端。
  4. JwtAuthorizationFilter:用于处理每个HTTP请求,从请求头中提取JWT并验证其有效性。
  5. SecurityConfig:配置Spring Security,包括禁用CSRF、配置无状态会话、添加JWT过滤器等。
  6. AuthController:简单的控制器,提供一个受保护的API端点。

JWT的优势与劣势

优势

  1. 无状态:JWT是无状态的,不需要在服务器端存储会话信息,减轻了服务器的负担。
  2. 跨域支持:由于JWT是通过HTTP头部传递的,天然支持跨域认证。
  3. 灵活性高:JWT的负载部分可以包含各种声明信息,便于扩展。
  4. 性能好:由于不需要在服务器端存储会话信息,JWT的性能较好。

劣势

  1. 令牌泄露风险:如果JWT泄露,攻击者可以伪装成合法用户访问受保护资源。
  2. 令牌不可撤销:JWT一旦签发,直到过期前都有效,无法主动撤销。
  3. 负载信息公开:JWT的负载部分是Base64编码的,可以被解码查看,但敏感信息需要加密保护。

JWT的最佳实践

  1. 设置合理的过期时间:JWT应该有合理的过期时间,避免长期有效带来的安全风险。
  2. 使用安全的签名算法:推荐使用HMAC SHA256或RSA等安全的签名算法,确保JWT的完整性。
  3. 保护JWT的存储:在客户端应妥善存储JWT,如使用HttpOnly的Cookie,防止XSS攻击。
  4. 定期更新密钥:定期更新JWT的签名密钥,增强安全性。
  5. 结合其他安全措施:JWT应结合其他安全措施使用,如SSL/TLS、CSRF防护等。

实际应用中的示例

为了更好地理解JWT的使用,我们将构建一个更加复杂的应用程序示例,包括用户注册、登录、权限管理等功能。

项目结构

项目的结构如下:

  1. jwt-advanced-demo
  2. ├── src
  3. ├── main
  4. ├── java
  5. ├── com
  6. ├── example
  7. ├── JwtAdvancedDemoApplication.java
  8. ├── config
  9. └── SecurityConfig.java
  10. ├── controller
  11. └── UserController.java
  12. ├── model
  13. └── User.java
  14. └── Role.java
  15. ├── repository
  16. └── UserRepository.java
  17. └── RoleRepository.java
  18. ├── service
  19. └── UserService.java
  20. └── RoleService.java
  21. ├── security
  22. ├── JwtAuthenticationFilter.java
  23. ├── JwtAuthorizationFilter.java
  24. ── JwtUtil.java
  25. └── UserDetailsServiceImpl.java
  26. ├── resources
  27. └── application.properties
  28. └── test
  29. └── java
  30. └── com
  31. └── example
  32. └── JwtAdvancedDemoApplicationTests.java

代码实现

JwtAdvancedDemoApplication.java
  1. package com.example;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. @SpringBootApplication
  5. public class JwtAdvancedDemoApplication {
  6. public static void main(String[] args) {
  7. SpringApplication.run(JwtAdvancedDemoApplication.class, args);
  8. }
  9. }
application.properties
  1. spring.datasource.url=jdbc:h2:mem:testdb
  2. spring.datasource.driverClassName=org.h2.Driver
  3. spring.datasource.username=sa
  4. spring.datasource.password=password
  5. spring.h2.console.enabled=true
  6. spring.jpa.hibernate.ddl-auto=update
User.java
  1. package com.example.model;
  2. import javax.persistence.*;
  3. import java.util.Set;
  4. @Entity
  5. public class User {
  6. @Id
  7. @GeneratedValue(strategy = GenerationType.IDENTITY)
  8. private Long id;
  9. private String username;
  10. private String password;
  11. @ManyToMany(fetch = FetchType.EAGER)
  12. @JoinTable(
  13. name = "user_roles",
  14. joinColumns = @JoinColumn(name = "user_id"),
  15. inverseJoinColumns = @JoinColumn(name = "role_id"))
  16. private Set<Role> roles;
  17. // Getters and setters
  18. }
Role.java
  1. package com.example.model;
  2. import javax.persistence.*;
  3. import java.util.Set;
  4. @Entity
  5. public class Role {
  6. @Id
  7. @GeneratedValue(strategy = GenerationType.IDENTITY)
  8. private Long id;
  9. private String name;
  10. @ManyToMany(mappedBy = "roles")
  11. private Set<User> users;
  12. // Getters and setters
  13. }
UserRepository.java
  1. package com.example.repository;
  2. import com.example.model.User;
  3. import org.springframework.data.jpa.repository.JpaRepository;
  4. public interface UserRepository extends JpaRepository<User, Long> {
  5. User findByUsername(String username);
  6. }
RoleRepository.java
  1. package com.example.repository;
  2. import com.example.model.Role;
  3. import org.springframework.data.jpa.repository.JpaRepository;
  4. public interface RoleRepository extends JpaRepository<Role, Long> {
  5. Role findByName(String name);
  6. }
UserDetailsServiceImpl.java
  1. package com.example.security;
  2. import com.example.model.User;
  3. import com.example.repository.UserRepository;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.security.core.userdetails.UserDetails;
  6. import org.springframework.security.core.userdetails.UserDetailsService;
  7. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  8. import org.springframework.stereotype.Service;
  9. @Service
  10. public class UserDetailsServiceImpl implements UserDetailsService {
  11. @Autowired
  12. private UserRepository userRepository;
  13. @Override
  14. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  15. User user = userRepository.findByUsername(username);
  16. if (user == null) {
  17. throw new UsernameNotFoundException("User not found");
  18. }
  19. return org.springframework.security.core.userdetails.User
  20. .withUsername(user.getUsername())
  21. .password(user.getPassword())
  22. .authorities(user.getRoles().stream()
  23. .map(role -> role.getName())
  24. .toArray(String[]::new))
  25. .build();
  26. }
  27. }
JwtUtil.java
  1. package com.example.security;
  2. import io.jsonwebtoken.Claims;
  3. import io.jsonwebtoken.Jwts;
  4. import io.jsonwebtoken.SignatureAlgorithm;
  5. import org.springframework.stereotype.Component;
  6. import java.util.Date;
  7. import java.util.HashMap;
  8. import java.util.Map;
  9. import java.util.function.Function;
  10. @Component
  11. public class JwtUtil {
  12. private String SECRET_KEY = "secret";
  13. public String extractUsername(String token) {
  14. return extractClaim(token, Claims::getSubject);
  15. }
  16. public Date extractExpiration(String token) {
  17. return extractClaim(token, Claims::getExpiration);
  18. }
  19. public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
  20. final Claims claims = extractAllClaims(token);
  21. return claimsResolver.apply(claims);
  22. }
  23. private Claims extractAllClaims(String token) {
  24. return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
  25. }
  26. private Boolean isTokenExpired(String token) {
  27. return extractExpiration(token).before(new Date());
  28. }
  29. public String generateToken(UserDetails userDetails) {
  30. Map<String, Object> claims = new HashMap<>();
  31. return createToken(claims, userDetails.getUsername());
  32. }
  33. private String createToken(Map<String, Object> claims, String subject) {
  34. return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
  35. .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
  36. .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
  37. }
  38. public Boolean validateToken(String token, UserDetails userDetails) {
  39. final String username = extractUsername(token);
  40. return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
  41. }
  42. }
JwtAuthenticationFilter.java
  1. package com.example.security;
  2. import org.springframework.security.authentication.AuthenticationManager;
  3. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  4. import org.springframework.security.core.Authentication;
  5. import org.springframework.security.core.AuthenticationException;
  6. import org.springframework.security.core.context.SecurityContextHolder;
  7. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  8. import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
  9. import javax.servlet.FilterChain;
  10. import javax.servlet.ServletException;
  11. import javax.servlet.http.HttpServletRequest;
  12. import javax.servlet.http.HttpServletResponse;
  13. import java.io.IOException;
  14. import java.util.ArrayList;
  15. public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
  16. private AuthenticationManager authenticationManager;
  17. private JwtUtil jwtUtil;
  18. public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
  19. this.authenticationManager = authenticationManager;
  20. this.jwtUtil = jwtUtil;
  21. setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
  22. }
  23. @Override
  24. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  25. String username = request.getParameter("username");
  26. String password = request.getParameter("password");
  27. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
  28. return authenticationManager.authenticate(authRequest);
  29. }
  30. @Override
  31. protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
  32. String username = authResult.getName();
  33. String token = jwtUtil.generateToken(authResult.getPrincipal());
  34. response.addHeader("Authorization", "Bearer " + token);
  35. }
  36. }
JwtAuthorizationFilter.java
  1. package com.example.security;
  2. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  3. import org.springframework.security.core.context.SecurityContextHolder;
  4. import org.springframework.security.core.userdetails.UserDetails;
  5. import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
  6. import org.springframework.web.filter.OncePerRequestFilter;
  7. import javax.servlet.FilterChain;
  8. import javax.servlet.ServletException;
  9. import javax.servlet.http.HttpServletRequest;
  10. import javax.servlet.http.HttpServletResponse;
  11. import java.io.IOException;
  12. public class JwtAuthorizationFilter extends OncePerRequestFilter {
  13. private UserDetailsServiceImpl userDetailsService;
  14. private JwtUtil jwtUtil;
  15. public JwtAuthorizationFilter(UserDetailsServiceImpl userDetailsService, JwtUtil jwtUtil) {
  16. this.userDetailsService = userDetailsService;
  17. this.jwtUtil = jwtUtil;
  18. }
  19. @Override
  20. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
  21. String authorizationHeader = request.getHeader("Authorization");
  22. String username = null;
  23. String jwt = null;
  24. if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
  25. jwt = authorizationHeader.substring(7);
  26. username = jwtUtil.extractUsername(jwt);
  27. }
  28. if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
  29. UserDetails userDetails = userDetailsService.loadUserByUsername(username);
  30. if (jwtUtil.validateToken(jwt, userDetails)) {
  31. UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
  32. usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
  33. SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
  34. }
  35. }
  36. chain.doFilter(request, response);
  37. }
  38. }
SecurityConfig.java
  1. package com.example.config;
  2. import com.example.security.JwtAuthenticationFilter;
  3. import com.example.security.JwtAuthorizationFilter;
  4. import com.example.security.JwtUtil;
  5. import com.example.security.UserDetailsServiceImpl;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.security.authentication.AuthenticationManager;
  10. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  11. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  12. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  13. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  14. import org.springframework.security.config.http.SessionCreationPolicy;
  15. import org.springframework.security.core.userdetails.UserDetailsService;
  16. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  17. import org.springframework.security.crypto.password.PasswordEncoder;
  18. @Configuration
  19. @EnableWebSecurity
  20. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  21. @Autowired
  22. private UserDetailsServiceImpl userDetailsService;
  23. @Autowired
  24. private JwtUtil
  25. jwtUtil;
  26. @Override
  27. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  28. auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  29. }
  30. @Override
  31. @Bean
  32. public AuthenticationManager authenticationManagerBean() throws Exception {
  33. return super.authenticationManagerBean();
  34. }
  35. @Override
  36. protected void configure(HttpSecurity http) throws Exception {
  37. http.csrf().disable()
  38. .authorizeRequests().antMatchers("/login").permitAll()
  39. .anyRequest().authenticated()
  40. .and()
  41. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  42. http.addFilter(new JwtAuthenticationFilter(authenticationManagerBean(), jwtUtil));
  43. http.addFilterBefore(new JwtAuthorizationFilter(userDetailsService, jwtUtil), JwtAuthenticationFilter.class);
  44. }
  45. @Bean
  46. public PasswordEncoder passwordEncoder() {
  47. return new BCryptPasswordEncoder();
  48. }
  49. }
UserController.java
  1. package com.example.controller;
  2. import com.example.model.Role;
  3. import com.example.model.User;
  4. import com.example.service.RoleService;
  5. import com.example.service.UserService;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.security.crypto.password.PasswordEncoder;
  8. import org.springframework.web.bind.annotation.*;
  9. import java.util.HashSet;
  10. import java.util.List;
  11. import java.util.Set;
  12. @RestController
  13. @RequestMapping("/users")
  14. public class UserController {
  15. @Autowired
  16. private UserService userService;
  17. @Autowired
  18. private RoleService roleService;
  19. @Autowired
  20. private PasswordEncoder passwordEncoder;
  21. @GetMapping
  22. public List<User> getAllUsers() {
  23. return userService.findAll();
  24. }
  25. @PostMapping("/register")
  26. public User registerUser(@RequestBody User user) {
  27. user.setPassword(passwordEncoder.encode(user.getPassword()));
  28. Set<Role> roles = new HashSet<>();
  29. roles.add(roleService.findByName("ROLE_USER"));
  30. user.setRoles(roles);
  31. return userService.save(user);
  32. }
  33. }
UserService.java
  1. package com.example.service;
  2. import com.example.model.User;
  3. import com.example.repository.UserRepository;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.stereotype.Service;
  6. import java.util.List;
  7. @Service
  8. public class UserService {
  9. @Autowired
  10. private UserRepository userRepository;
  11. public List<User> findAll() {
  12. return userRepository.findAll();
  13. }
  14. public User save(User user) {
  15. return userRepository.save(user);
  16. }
  17. }
RoleService.java
  1. package com.example.service;
  2. import com.example.model.Role;
  3. import com.example.repository.RoleRepository;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.stereotype.Service;
  6. @Service
  7. public class RoleService {
  8. @Autowired
  9. private RoleRepository roleRepository;
  10. public Role findByName(String name) {
  11. return roleRepository.findByName(name);
  12. }
  13. }

详细解读

在这个更复杂的示例中,我们添加了用户注册功能和角色管理。以下是关键组件的详细解读:

  1. UserRole 模型:定义了用户和角色实体类,用户可以拥有多个角色。
  2. UserRepositoryRoleRepository:定义了用户和角色的JPA仓库接口,用于数据库操作。
  3. UserDetailsServiceImpl:实现Spring Security的UserDetailsService接口,用于从数据库中加载用户信息。
  4. JwtUtil:用于生成和解析JWT,包括提取用户名、验证令牌、生成令牌等功能。
  5. JwtAuthenticationFilter:用于处理用户登录请求,验证用户名和密码,生成JWT并返回给客户端。
  6. JwtAuthorizationFilter:用于处理每个HTTP请求,从请求头中提取JWT并验证其有效性。
  7. SecurityConfig:配置Spring Security,包括禁用CSRF、配置无状态会话、添加JWT过滤器等。
  8. UserController:提供用户注册和查询功能,注册时自动分配用户角色。
  9. UserServiceRoleService:提供用户和角色的业务逻辑处理。

总结

通过本文,我们详细介绍了Spring Security中如何集成和使用JSON Web Token(JWT)。我们探讨了JWT的基本概念和工作原理,如何在Spring Security中实现JWT认证与授权,并通过详细的示例展示了如何在实际应用中使用这些技术。

JWT作为一种无状态、分布式的认证机制,具有灵活性高、性能好、跨域支持等优势,广泛应用于现代Web应用程序中。通过合理配置和使用JWT,开发者可以构建出更加安全、高效的Web应用程序。

在实际应用中,开发者应根据具体的业务需求和安全要求,灵活配置和使用Spring Security中的JWT功能,从而构建出更加安全和可靠的Web应用程序。通过遵循最佳实践,可以有效提高应用程序的安全性和性能,确保其在复杂的场景下仍能保持高水平的安全保护。