在 Spring Boot 中使用 JWT 构建基于角色的访问控制系统

2025-06-08

在 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>
Enter fullscreen mode Exit fullscreen mode

这是完整的 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>
Enter fullscreen mode Exit fullscreen mode

应用程序.属性

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
Enter fullscreen mode Exit fullscreen mode

这是 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=updateHibernate 配置为根据实体类自动更新数据库模式。
  • 该行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

Enter fullscreen mode Exit fullscreen mode

让我们开始吧!

现在我们开始了解配置包

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() {}

}
Enter fullscreen mode Exit fullscreen mode

此类实现了 Filter 接口,该接口是 Java Servlet API 的一部分。过滤器用于对 HTTP 请求和响应在应用程序传递过程中执行操作。

  • doFilter 方法:实现 Filter 接口时需要此方法。每个传入的 HTTP 请求都会调用此方法。在此方法内部,代码负责向 HTTP 响应添加必要的 CORS 标头。CORS 标头用于控制和定义跨域请求的策略。它们指定谁可以访问网页资源,以及允许从不同来源执行哪些操作。此方法中的代码设置了各种 CORS 标头,例如Access-Control-Allow-OriginAccess-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();
    }
}
Enter fullscreen mode Exit fullscreen mode

@Configuration:指示此类包含应用程序的配置设置。
@EnableWebSecurity:为 Web 应用程序启用 Spring Security 功能。
@EnableGlobalMethodSecurity(prePostEnabled = true):启用方法级安全注解,例如@PreAuthorize@PostAuthorize

@Resource:注入UserDetailsServiceBean,可能负责与用户相关的操作。注入和Bean
@Autowired:的实例,分别用于密码哈希和处理未经授权的访问。PasswordEncoderUnauthorizedEntryPoint

configure(AuthenticationManagerBuilder auth) Method:

  • 此方法配置身份验证管理器。
  • 它指定该userDetailsServicebean 应用于用户身份验证并设置密码编码器。

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);
    }

}
Enter fullscreen mode Exit fullscreen mode

@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);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 此类扩展了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();
    }
}
Enter fullscreen mode Exit fullscreen mode
  • BCryptPasswordEncoder是 Spring Security 框架中一个流行的密码哈希库。它用于安全地哈希和验证密码。
  • 在此配置中,BCryptPasswordEncoder该方法会创建并返回一个 bean encoder()。然后,该 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");
    }

}
Enter fullscreen mode Exit fullscreen mode

该类实现了AuthenticationEntryPointSpring Security 接口。该AuthenticationEntryPoint接口负责处理与身份验证相关的异常,尤其是未经授权的访问。

  • commence方法是此类的主要方法,在 HTTP 请求期间发生身份验证异常时调用该方法。
  • 它需要三个参数:

    1. HttpServletRequest request:表示传入的 HTTP
    请求。
    2. :表示 将发送回客户端的HttpServletResponse responseHTTP 响应。 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;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 这定义了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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

登录用户

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

用户数据

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

用户服务

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);

}
Enter fullscreen mode Exit fullscreen mode

现在我们将实现我们的服务逻辑
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;
    }
}

Enter fullscreen mode Exit fullscreen mode

用户服务实现

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

让我们创建我们的控制器类

用户的初始步骤是完成注册过程。用户至少需要提供用户名和密码。通过调用服务方法保存用户信息,这一重要步骤就完成了。

为了安全地访问应用程序的 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

这些方法演示了使用 Spring Security 的注释的基于角色的访问控制@PreAuthorizeadminPing只能由具有“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');
Enter fullscreen mode Exit fullscreen mode

最后,您可以在 Postman 中测试您的 API,我已经为Postman
创建了集合

这是我的github中的完整代码

如果您有任何疑问,请随时通过Instagram联系我。
感谢您的阅读!祝您
编码愉快!

链接:https://dev.to/alphaaman/building-a-role-based-access-control-system-with-jwt-in-spring-boot-a7l
PREV
为什么 32 位称为 x86 而不是 x32?
NEXT
我做了一个开源平台来学习计算机科学。如果你对 MERN 技术栈感兴趣,可以看看这个。