在 Spring Boot 中使用 JWT 构建基于角色的访问控制系统
介绍
欢迎来到我的博客,在这里我们将踏上一段激动人心的 Web 应用安全之旅!如果您是 Spring Boot 的新手,或者刚刚开始探索复杂的身份验证和授权机制,那么您来对地方了。在这篇开篇文章中,我很高兴与大家分享我对一个与各级开发人员息息相关的主题的看法:在 Spring Boot 中使用 JWT 构建基于角色的访问控制系统。
在本系列博客中,我们将深入探讨 Spring Security 的世界,并探索 JSON Web Tokens (JWT) 如何增强其功能。我将逐步指导您,从使用可靠的 Spring Boot 设置奠定基础,一直到使用 JWT 令牌实现基于角色的访问控制。
在此博客中,我们将讨论以下主题:
- 创建不同的角色并将其分配给用户
- 注册用户和管理员
- 生成 JWT 令牌和身份验证
- 执行基于角色的操作
先决条件
在我们深入探讨如何在 Spring Boot 中使用 JWT 构建基于角色的访问控制系统之前,请确保您已准备好必要的工具和环境。以下是您需要做的准备:
Java Development Kit (JDK)
:确保您的计算机上安装了兼容版本的 JDK。Spring Boot 通常与 JDK 8 或更高版本兼容。
Integrated Development Environment (IDE)
:选择适合您偏好的 IDE。IntelliJ IDEA 和 Eclipse 是 Spring Boot 开发的热门选择。
Maven or Gradle
:您需要 Maven 或 Gradle 作为构建工具来管理项目依赖项。
设置环境:
Spring Boot 项目初始化:使用此处的Spring Initializr Web 工具或 IDE 的项目创建向导创建一个新的 Spring Boot 项目。
以下是我的设置,您可以遵循:
项目:Maven
Spring Boot 版本:2.7.3(如果不可用,则使用 3.1.3,然后稍后在 pom.xml 中更改它)
Java 版本:17
依赖项:
1.Spring Data JPA
2.Spring web
3.Spring Security
4.MySQL Driver
5.JSON WEB TOKEN
你不会在那里找到 JSON web Token,你必须手动添加它
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
这是完整的 pom.xml
<?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 https://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.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.alpha</groupId>
<artifactId>alpha</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>alpha</name>
<description>Demo project for Spring Boot Role based Authentication using JWT </description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
应用程序.属性
jwt.token.validity=18000
jwt.signing.key=YourSignInKey
jwt.authorities.key=roles
jwt.token.prefix=Bearer
jwt.header.string=Authorization
spring.datasource.url=jdbc:mysql://localhost:3306/yourdb
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.user.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
server.port=8080
这是 Spring Boot 应用程序的配置文件。
- 第一行将 JSON Web Tokens (JWT) 的有效期设置为 18000 秒(5 小时)。
- 第二行指定用于生成和验证 JWT 的签名密钥。
- 第三行定义从 JWT 中提取权限/角色的密钥。
- 第四行将 JWT 令牌的前缀设置为“Bearer”。
- 第五行指定 HTTP 请求中用于 JWT 令牌的标头字符串。
- 接下来的几行配置应用程序的 MySQL 数据库连接,包括 URL、用户名和密码。
- 该行
spring.jpa.show-sql=true
可以显示 Hibernate 执行的 SQL 语句。 - 该行将
spring.jpa.hibernate.ddl-auto=update
Hibernate 配置为根据实体类自动更新数据库模式。 - 该行
spring.user.datasource.driver-class-name=com.mysql.jdbc.Driver
指定了用户数据源的驱动程序类。 - 该行
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
设置了 MySQL 的 Hibernate 方言。 - 最后一行将服务器端口设置为 8080。
“如您所见,这里是清晰的文件夹结构。”
src
└── main
├── java
│ └── com
│ └── alpha
│ ├── config (Package)
│ │ ├── JwtAuthenticationFilter.java
│ │ ├── PasswordEncoder.java
│ │ ├── TokenProvider.java
│ │ ├── UnauthorizedEntryPoint.java
│ │ └── WebSecurityConfig.java
│ ├── controller (Package)
│ │ └── UserController.java
│ ├── dao (Package)
│ │ ├── RoleDao.java
│ │ └── UserDao.java
│ ├── model (Package)
│ │ ├── AuthToken.java
│ │ ├── LoginUser.java
│ │ ├── Role.java
│ │ ├── User.java
│ │ └── UserDto.java
│ ├── service (Package)
│ │ ├── impl
│ │ │ ├── RoleServiceImpl.java
│ │ │ └── UserServiceImpl.java
│ │ ├── RoleService.java
│ │ └── UserService.java
│ └── AlphaApplication.java
└── resources
└── application.properties
让我们开始吧!
现在我们开始了解配置包
Spring Security 是 Spring 框架为 Java 应用程序提供的一个功能强大且高度可定制的安全框架。它的主要目的是处理 Web 和企业应用程序中的身份验证、授权和各种安全问题。Spring Security 通常用于保护 Web 应用程序、RESTful API 以及软件系统的其他组件。
身份验证:
身份验证是确认个人或实体身份的过程。它确保个人或实体在授予访问权限之前与其声称的身份相符。这就像在允许某人进入安全区域之前检查其身份证一样。
想象一下一场职业板球比赛。运动员在踏入赛场之前,必须证明自己的身份。他们通过出示印有姓名、照片和唯一 ID 号的正式球员卡来证明身份。比赛官员(例如裁判和队长)会检查球员卡,确保其与球员的外貌相符,并且该球员在授权球员名单上。确认身份后,该球员即通过身份验证,并被允许参加比赛。
授权:
授权在身份验证之后进行,用于确定经过身份验证的个人或实体可以访问或执行哪些操作或资源。它根据角色、权限或规则指定访问和控制级别。
一旦板球运动员通过身份验证并上场,授权即生效。每位运动员都有特定的角色(例如击球手、投球手、守场员),并可执行相关操作。例如,投球手有权投球,击球手有权击球,守门员有权守三柱门。教练或队长可能拥有特殊授权,可以在比赛中做出战略决策,例如更改击球顺序或场上位置。
让我们逐个查看配置文件
CORSFilter :
CORSFilter 类负责处理 Web 应用中与 CORS 相关的设置。它会拦截传入的 HTTP 请求,将必要的 CORS 标头添加到响应中,然后使用 chain.doFilter(req, res) 允许请求继续处理。此过滤器有助于控制和保护 Web 应用中不同来源访问服务器上资源的方式。
package com.alpha.config;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CORSFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Authorization, Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers");
chain.doFilter(req, res);
}
public void init(FilterConfig filterConfig) {}
public void destroy() {}
}
此类实现了 Filter 接口,该接口是 Java Servlet API 的一部分。过滤器用于对 HTTP 请求和响应在应用程序传递过程中执行操作。
- doFilter 方法:实现 Filter 接口时需要此方法。每个传入的 HTTP 请求都会调用此方法。在此方法内部,代码负责向 HTTP 响应添加必要的 CORS 标头。CORS 标头用于控制和定义跨域请求的策略。它们指定谁可以访问网页资源,以及允许从不同来源执行哪些操作。此方法中的代码设置了各种 CORS 标头,例如
Access-Control-Allow-Origin
、Access-Control-Allow-Methods
等。这些标头规定了哪些域可以访问资源、允许使用哪些 HTTP 方法以及其他与 CORS 相关的策略。
该init
方法用于过滤器首次创建时可能需要的初始化任务。
destroy
当过滤器被移除或关闭时,会调用该方法。
WebSecurityConfig
此类负责在 Spring Boot Web 应用程序中配置安全设置,例如身份验证、授权和请求过滤。它还集成了自定义组件(例如 JwtAuthenticationFilter),以处理特定的安全
需求。此配置是使用 Spring Security 保护 RESTful API 或 Web 应用程序的常用设置。
package com.alpha.config;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource(name = "userService")
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder encoder;
@Autowired
private UnauthorizedEntryPoint unauthorizedEntryPoint;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(encoder.encoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers("/users/authenticate", "/users/register").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationFilter();
}
}
@Configuration:
指示此类包含应用程序的配置设置。@EnableWebSecurity:
为 Web 应用程序启用 Spring Security 功能。@EnableGlobalMethodSecurity(prePostEnabled = true):
启用方法级安全注解,例如@PreAuthorize
和@PostAuthorize
。
@Resource:
注入UserDetailsService
Bean,可能负责与用户相关的操作。注入和Bean@Autowired:
的实例,分别用于密码哈希和处理未经授权的访问。PasswordEncoder
UnauthorizedEntryPoint
configure(AuthenticationManagerBuilder auth) Method:
- 此方法配置身份验证管理器。
- 它指定该
userDetailsService
bean 应用于用户身份验证并设置密码编码器。
configure(HttpSecurity http) Method:
- 此方法配置 HTTP 安全设置。
- 它包括 CORS(跨域资源共享)、CSRF(跨站点请求伪造)和 URL 权限的设置。
- 它指定哪些 URL 无需身份验证即可访问(“/users/authenticate”和“/users/register”),并要求对所有其他请求进行身份验证。
- 它设置了处理未经授权访问的身份验证入口点,并将会话管理策略定义为无状态。
- addFilterBefore 方法在处理 JWT(JSON Web Token)身份验证
JwtAuthenticationFilter
之前添加一个自定义的过滤器UsernamePasswordAuthenticationFilter
。
authenticationManagerBean() Method:
- 该方法声明一个AuthenticationManager bean,用于用户身份验证。
JwtAuthenticationFilter Bean:
- 此方法为 JwtAuthenticationFilter 声明一个 bean,它是用于基于 JWT 的身份验证的自定义过滤器。
TokenProvider
此类负责处理 Spring Boot 应用程序安全流程中的 JWT。它可以生成令牌、从令牌中提取用户信息、验证令牌,并根据 JWT 中存储的信息为用户创建身份验证令牌。
package com.alpha.config;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
public class TokenProvider implements Serializable {
@Value("${jwt.token.validity}")
public long TOKEN_VALIDITY;
@Value("${jwt.signing.key}")
public String SIGNING_KEY;
@Value("${jwt.authorities.key}")
public String AUTHORITIES_KEY;
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) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(SIGNING_KEY)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALIDITY*1000))
.signWith(SignatureAlgorithm.HS256, SIGNING_KEY)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
UsernamePasswordAuthenticationToken getAuthenticationToken(final String token, final Authentication existingAuth, final UserDetails userDetails) {
final JwtParser jwtParser = Jwts.parser().setSigningKey(SIGNING_KEY);
final Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);
final Claims claims = claimsJws.getBody();
final Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}
}
@Component:
将此类标记为 Spring 组件,允许其在 Spring 应用程序上下文中被自动扫描并注册为 bean。
- getUsernameFromToken(String token):此方法从 JWT 令牌中提取用户名(主题)。
- getExpirationDateFromToken(String token):从 JWT 令牌中检索到期日期。
- getClaimFromToken(String token,Function claimsResolver):从 JWT 令牌中提取声明的通用方法。
- getAllClaimsFromToken(String token):从 JWT 令牌解析并检索所有声明(有效负载)。
- isTokenExpired(String token):根据到期日期检查 JWT 令牌是否已过期。
- generateToken(Authentication authentication):根据提供的 Authentication 对象生成一个新的 JWT 令牌。它包含主体(用户名)、权限、颁发时间和到期时间。
- validToken(String token, UserDetails userDetails):根据提供的 UserDetails 验证 JWT 令牌。它会检查令牌的主题是否与用户的用户名匹配,以及令牌是否已过期。
- getAuthenticationToken(final String token, final Authentication existingAuth, final UserDetails userDetails):此方法解析 JWT 令牌,并创建包含用户权限的 Authentication 对象。它用于基于 JWT 令牌对用户进行身份验证。
JwtAuthenticationFilter
负责拦截传入的请求,从请求头中提取 JWT,并根据令牌对用户进行身份验证。它确保经过身份验证的用户设置了其安全上下文,从而允许他们访问应用程序中受保护的资源。
package com.alpha.config;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Value("${jwt.header.string}")
public String HEADER_STRING;
@Value("${jwt.token.prefix}")
public String TOKEN_PREFIX;
@Resource(name = "userService")
private UserDetailsService userDetailsService;
@Autowired
private TokenProvider jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
String username = null;
String authToken = null;
if (header != null && header.startsWith(TOKEN_PREFIX)) {
authToken = header.replace(TOKEN_PREFIX, "");
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
logger.error("Error occurred while retrieving Username from Token", e);
} catch (ExpiredJwtException e) {
logger.warn("The token has expired", e);
} catch (SignatureException e) {
logger.error("Authentication Failed. Invalid username or password.");
}
} else {
logger.warn("Bearer string not found, ignoring the header");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = jwtTokenUtil.getAuthenticationToken(authToken, SecurityContextHolder.getContext().getAuthentication(), userDetails);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("User authenticated: " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}
}
- 此类扩展了
OncePerRequestFilter
,确保每个请求仅应用一次此过滤器。 doFilterInternal
方法:此方法是过滤器的核心,每个传入的 HTTP 请求都会调用该方法。- 该方法首先检查请求标头中是否存在 JWT,以及它是否以定义的令牌前缀(“Bearer”)开头。
- 如果找到有效的令牌,它会尝试使用 TokenProvider 类从令牌中提取用户名。
- 它捕获各种与令牌相关的错误异常,例如令牌过期(
ExpiredJwtException
)和无效签名(SignatureException
),并记录它们。 - 如果从令牌中获取了有效的用户名并且没有现有的身份验证上下文,它会根据
UserDetailsService
用户名加载用户详细信息。 - 然后,它使用 验证令牌与用户详细信息是否一致
TokenProvider
。如果令牌有效,它将创建一个UsernamePasswordAuthenticationToken
包含用户详细信息的 ,并设置身份验证详细信息。 - 最后,它使用设置经过身份验证的用户的安全上下文
SecurityContextHolder
。 - 处理身份验证后,过滤器通过调用继续处理请求
chain.doFilter(req, res)
。
BCryptPasswordEncoder
此 bean 可在整个应用程序中使用,用于安全地散列和验证密码,尤其是在用户身份验证和安全环境中。在 Spring 应用程序中配置和管理此类密码编码组件以增强功能是一种常见的做法security
。
package com.alpha.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class PasswordEncoder {
@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
}
BCryptPasswordEncoder
是 Spring Security 框架中一个流行的密码哈希库。它用于安全地哈希和验证密码。- 在此配置中,
BCryptPasswordEncoder
该方法会创建并返回一个 beanencoder()
。然后,该 bean 可以注入到应用程序的其他部分(例如 Spring Security 配置),以处理密码编码和解码。
UnauthorizedEntryPoint
UnauthorizedEntryPoint 类负责处理启用 Spring Security 的应用程序中对受保护资源的未经授权的访问。当未经身份验证的用户尝试访问受保护的资源时,此类会发送状态码为 401 的 HTTP 响应,表示该请求缺少有效的身份验证。此响应告知客户端需要提供正确的身份验证凭据才能访问该资源。
package com.alpha.config;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
@Component
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthenticated");
}
}
该类实现了AuthenticationEntryPoint
Spring Security 接口。该AuthenticationEntryPoint
接口负责处理与身份验证相关的异常,尤其是未经授权的访问。
- 该
commence
方法是此类的主要方法,在 HTTP 请求期间发生身份验证异常时调用该方法。 -
它需要三个参数:
1.
HttpServletRequest request
:表示传入的 HTTP
请求。
2. :表示 将发送回客户端的HttpServletResponse response
HTTP 响应。 3. :表示 发生的身份验证异常,通常是由于 未经授权的访问造成的。AuthenticationException authException
-
在此方法中,它会发送一个 HTTP 响应,其状态码为
401 Unauthorized
,消息为“未经过身份验证”。这是表示请求缺少有效的身份验证凭据或授权的标准响应。
好的,让我们从模型类开始
User
类表示具有用户名、密码、邮箱、电话、姓名和角色等属性的用户实体。它还与 Role 实体定义了多对多关系,允许用户拥有多个角色。
package com.alpha.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.util.Set;
@Entity
public class User {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private long id;
@Column
private String username;
@Column
@JsonIgnore
private String password;
@Column
private String email;
@Column
private String phone;
@Column
private String name;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "USER_ROLES",
joinColumns = {
@JoinColumn(name = "USER_ID")
},
inverseJoinColumns = {
@JoinColumn(name = "ROLE_ID") })
private Set<Role> roles;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
- 这定义了
many-to-many
用户实体和角色实体之间的关系。用户可以拥有多个角色,并且角色可以与多个用户关联。 - 该
@ManyToMany
注释表明了多对多的关系。 - 该
fetch = FetchType.EAGER
属性指定在加载用户时应立即获取角色。 - 该
cascade = CascadeType.ALL
属性指定如果对 User 执行诸如持久化、合并、删除等操作entity
,则相同的操作应该级联到其关联的 Role 实体。 - 该
@JoinTable
注解用于定义保存用户和角色之间关系的连接表的名称。它指定用于连接表的列。
Role
类表示具有名称和描述等属性的用户角色实体。它被设计为持久化存储在数据库表中,并可以通过多对多关系与用户关联,正如 User 实体引用 Role 的关系映射所示。
package com.alpha.model;
import javax.persistence.*;
@Entity
public class Role {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private long id;
@Column
private String name;
@Column
private String description;
// Getter for id
public long getId() {
return id;
}
// Setter for id
public void setId(long id) {
this.id = id;
}
// Getter for name
public String getName() {
return name;
}
// Setter for name
public void setName(String name) {
this.name = name;
}
// Getter for description
public String getDescription() {
return description;
}
// Setter for description
public void setDescription(String description) {
this.description = description;
}
}
Role 类表示具有名称和描述等属性的用户角色实体。它被设计为持久化存储在数据库表中,并且可以通过多对多关系与用户关联,正如 User 实体引用 Role 的关系映射所示。
授权令牌
package com.alpha.model;
/**
* Represents an authentication token.
*/
public class AuthToken {
private String token;
/**
* Constructs a new AuthToken object.
*/
public AuthToken() {
}
/**
* Constructs a new AuthToken object with the specified token.
*
* @param token the authentication token
*/
public AuthToken(String token) {
this.token = token;
}
/**
* Returns the authentication token.
*
* @return the authentication token
*/
public String getToken() {
return token;
}
/**
* Sets the authentication token.
*
* @param token the authentication token to be set
*/
public void setToken(String token) {
this.token = token;
}
}
登录用户
package com.alpha.model;
public class LoginUser {
private String username;
private String password;
// Getters and Setters for username
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
// Getters and Setters for password
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
用户数据
package com.alpha.model;
public class UserDto {
private String username;
private String password;
private String email;
private String phone;
private String name;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User getUserFromDto(){
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setEmail(email);
user.setPhone(phone);
user.setName(name);
return user;
}
}
DAO
是 Spring Data JPA 存储库接口,通常用于对实体类执行 CRUD(创建、读取、更新、删除)操作。让我们分解一下它的关键组件:
UserDao
package com.alpha.dao;
import com.alpha.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserDao extends CrudRepository<User, Long> {
User findByUsername(String username);
}
RoleDao
package com.alpha.dao;
import com.alpha.model.Role;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleDao extends CrudRepository<Role, Long> {
Role findRoleByName(String name);
}
JPARepository
您也可以使用。
服务层
现在我们将为用户和角色服务定义服务接口。这些服务接口将作为实际服务实现的蓝图,并封装核心业务逻辑。
作为最佳实践,使用接口进行服务定义可以促进关注点分离,并允许轻松切换实现,例如在使用模拟框架进行测试时。RoleService
package com.alpha.service;
// Importing the Role model
import com.alpha.model.Role;
// Declaring the RoleService interface
public interface RoleService {
// Method to find a Role by its name
Role findByName(String name);
}
用户服务
package com.alpha.service;
import com.alpha.model.User;
import com.alpha.model.UserDto;
import java.util.List;
public interface UserService {
// Saves a user
User save(UserDto user);
// Retrieves all users
List<User> findAll();
// Retrieves a user by username
User findOne(String username);
User createEmployee(UserDto user);
}
现在我们将实现我们的服务逻辑
RoleServiceImpl
package com.alpha.service.impl;
import com.alpha.dao.RoleDao;
import com.alpha.model.Role;
import com.alpha.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service(value = "roleService")
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleDao roleDao;
@Override
public Role findByName(String name) {
// Find role by name using the roleDao
Role role = roleDao.findRoleByName(name);
return role;
}
}
用户服务实现
package com.alpha.service.impl;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.alpha.dao.UserDao;
import com.alpha.model.Role;
import com.alpha.model.User;
import com.alpha.model.UserDto;
import com.alpha.service.RoleService;
import com.alpha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService, UserService {
@Autowired
private RoleService roleService;
@Autowired
private UserDao userDao;
@Autowired
private BCryptPasswordEncoder bcryptEncoder;
// Load user by username
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findByUsername(username);
if(user == null){
throw new UsernameNotFoundException("Invalid username or password.");
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority(user));
}
// Get user authorities
private Set<SimpleGrantedAuthority> getAuthority(User user) {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
user.getRoles().forEach(role -> {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
});
return authorities;
}
// Find all users
public List<User> findAll() {
List<User> list = new ArrayList<>();
userDao.findAll().iterator().forEachRemaining(list::add);
return list;
}
// Find user by username
@Override
public User findOne(String username) {
return userDao.findByUsername(username);
}
// Save user
@Override
public User save(UserDto user) {
User nUser = user.getUserFromDto();
nUser.setPassword(bcryptEncoder.encode(user.getPassword()));
// Set default role as USER
Role role = roleService.findByName("USER");
Set<Role> roleSet = new HashSet<>();
roleSet.add(role);
// If email domain is admin.edu, add ADMIN role
if(nUser.getEmail().split("@")[1].equals("admin.edu")){
role = roleService.findByName("ADMIN");
roleSet.add(role);
}
nUser.setRoles(roleSet);
return userDao.save(nUser);
}
@Override
public User createEmployee(UserDto user) {
User nUser = user.getUserFromDto();
nUser.setPassword(bcryptEncoder.encode(user.getPassword()));
Role employeeRole = roleService.findByName("EMPLOYEE");
Role customerRole = roleService.findByName("USER");
Set<Role> roleSet = new HashSet<>();
if (employeeRole != null) {
roleSet.add(employeeRole);
}
if (customerRole != null) {
roleSet.add(customerRole);
}
nUser.setRoles(roleSet);
return userDao.save(nUser);
}
}
让我们创建我们的控制器类
用户的初始步骤是完成注册过程。用户至少需要提供用户名和密码。通过调用服务方法保存用户信息,这一重要步骤就完成了。
为了安全地访问应用程序的 API,用户必须包含服务器生成的 JWT(JSON Web Token)。所有必要的基础工作都已在我们的 中列出TokenProvider
。我们利用generateToken
方法并将生成的令牌包含在响应中,以确保对 API 的安全访问。UserController类
处理UserController
与用户相关的 HTTP 请求,包括注册和身份验证。它还演示了基于角色的特定资源访问控制。用户操作的实际业务逻辑委托给了 UserService 和TokenProvider
组件。
package com.alpha.controller;
import com.alpha.config.TokenProvider;
import com.alpha.model.AuthToken;
import com.alpha.model.LoginUser;
import com.alpha.model.User;
import com.alpha.model.UserDto;
import com.alpha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenProvider jwtTokenUtil;
@Autowired
private UserService userService;
/**
* Generates a token for the given user credentials.
*
* @param loginUser The user's login credentials.
* @return A response entity containing the generated token.
* @throws AuthenticationException if authentication fails.
*/
@RequestMapping(value = "/authenticate", method = RequestMethod.POST)
public ResponseEntity<?> generateToken(@RequestBody LoginUser loginUser) throws AuthenticationException {
final Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginUser.getUsername(),
loginUser.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
final String token = jwtTokenUtil.generateToken(authentication);
return ResponseEntity.ok(new AuthToken(token));
}
/**
* Saves a new user.
*
* @param user The user to be saved.
* @return The saved user.
*/
@RequestMapping(value="/register", method = RequestMethod.POST)
public User saveUser(@RequestBody UserDto user){
return userService.save(user);
}
/**
* Returns a message that can only be accessed by users with the 'ADMIN' role.
*
* @return A message that can only be accessed by admins.
*/
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value="/adminping", method = RequestMethod.GET)
public String adminPing(){
return "Only Admins Can Read This";
}
/**
* Returns a message that can be accessed by any user.
*
* @return A message that can be accessed by any user.
*/
@PreAuthorize("hasRole('USER')")
@RequestMapping(value="/userping", method = RequestMethod.GET)
public String userPing(){
return "Any User Can Read This";
}
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value="/create/employee", method = RequestMethod.POST)
public User createEmployee(@RequestBody UserDto user){
return userService.createEmployee(user);
}
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value="/find/all", method = RequestMethod.GET)
public List<User> getAllList(){
return userService.findAll();
}
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value="/find/by/username", method = RequestMethod.GET)
public User getAllList(@RequestParam String username){
return userService.findOne(username);
}
}
这些方法演示了使用 Spring Security 的注释的基于角色的访问控制@PreAuthorize
。adminPing
只能由具有“ADMIN”角色的用户访问,而userPing
可以由具有“USER”角色的用户访问。
在测试你的 API 之前,你需要在你的数据库中添加一些角色
INSERT INTO role (id, description, name) VALUES (1, 'Admin role', 'ADMIN');
INSERT INTO role (id, description, name) VALUES (2, 'Employee role', 'EMPLOYEE');
INSERT INTO role (id, description, name) VALUES (3, 'User role', 'USER');
最后,您可以在 Postman 中测试您的 API,我已经为Postman
创建了集合
这是我的github中的完整代码
如果您有任何疑问,请随时通过Instagram联系我。
感谢您的阅读!祝您
编码愉快!