使用 Spring-Boot 进行身份验证和授权
在浩瀚的 Web 开发世界中,身份验证是每个数字领域的守护者。在本教程中,我们将了解如何以原生方式并遵循框架的良好实践来保护、验证和授权 Spring-Boot 应用程序的用户。
我们将使用以下技术:
- Java 17
- Spring-boot 3.1.5
- jwt
- 休眠/JPA
- PostgreSQL
- 龙目岛
概括
第一步
为了保护我们的应用程序,我们需要两个依赖项pom.xml
,第一个是原生 Spring 安全包,另一个将帮助我们创建和验证我们的 JWT 令牌。
//pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
用户实体和存储库
首先,我们需要一个枚举来表示用户角色,这将帮助我们定义应用程序中每个用户的权限。
// enums/UserRole.java
public enum UserRole {
ADMIN("admin"),
USER("user");
private String role;
UserRole(String role) {
this.role = role;
}
public String getValue() {
return role;
}
}
在枚举中我们有两个代表角色:ADMIN
和USER
,ADMIN
角色将有权访问我们应用程序的所有端点,而USER
角色只能访问特定的端点。
用户实体将成为我们身份验证系统的核心,它将保存用户的凭证以及用户拥有的角色。我们将实现一个UserDetails
接口来表示我们的用户实体,该接口由 Spring Security 包提供,并且是 Spring Boot 应用程序中表示用户实体的推荐方式。
// entities/UserEntity.java
@Table()
@Entity(name = "users")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String login;
private String password;
@Enumerated(EnumType.STRING)
private UserRole role;
public User(String login, String password, UserRole role) {
this.login = login;
this.password = password;
this.role = role;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (this.role == UserRole.ADMIN) {
return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER"));
}
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getUsername() {
return login;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
它有很多方法可以重写来定制身份验证过程,您也可以在数据库中实现这些属性,但现在我们只使用使我们的身份验证系统工作所需的方法:id
,,username
和。password
role
对于用户存储库,我们有以下代码:
// repositories/UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
UserDetails findByLogin(String login);
}
扩展后,JpaRepository
我们将可以使用多种方法来操作数据库中的用户。此外,findByLogin
Spring Security 还将使用该方法在数据库中查找用户并验证其凭据。
令牌提供者
我们需要定义一个密钥来签名我们的令牌,该密钥将用于验证和生成令牌签名。我们将使用@Value
注释从文件中获取密钥application.yml
。在application.yml
文件中,我们将密钥定义为环境变量,这将有助于我们确保密钥的安全,并使其不被源代码访问。
//.env
JWT_SECRET="yoursecret"
在我们的application.yml
:
// resources/application.yml
security:
jwt:
token:
secret-key: ${JWT_SECRET}
要使 spring-boot 应用程序读取环境变量,我们需要PropertySource
在主类中声明注解,指明.env
文件的位置。在本例中,它位于项目的根目录中,因此我们将使用该user.dir
变量来获取项目根路径。主类如下所示:
@SpringBootApplication
@PropertySource("file:${user.dir}/.env")
public class SpringAuthApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAuthApplication.class, args);
}
}
最后我们可以定义我们的令牌提供程序类,该类将负责生成和验证我们的令牌。
// config/auth/TokenProvider.java
@Service
public class TokenProvider {
@Value("${security.jwt.token.secret-key}")
private String JWT_SECRET;
public String generateAccessToken(User user) {
try {
Algorithm algorithm = Algorithm.HMAC256(JWT_SECRET);
return JWT.create()
.withSubject(user.getUsername())
.withClaim("username", user.getUsername())
.withExpiresAt(genAccessExpirationDate())
.sign(algorithm);
} catch (JWTCreationException exception) {
throw new JWTCreationException("Error while generating token", exception);
}
}
public String validateToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(JWT_SECRET);
return JWT.require(algorithm)
.build()
.verify(token)
.getSubject();
} catch (JWTVerificationException exception) {
throw new JWTVerificationException("Error while validating token", exception);
}
}
private Instant genAccessExpirationDate() {
return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
}
}
在 中,generateAccessToken
我们定义了一个算法来签名我们的令牌、令牌的主题和到期日期,并返回一个新的令牌。在validateToken
方法 中,我们验证令牌签名并返回令牌的主题。
安全过滤器
然后,我们需要定义一个过滤器来拦截请求并验证令牌。我们将扩展OncePerRequestFilter
Spring Security 类来拦截请求并验证令牌。
// config/auth/SecurityFilter.java
@Component
public class SecurityFilter extends OncePerRequestFilter {
@Autowired
TokenProvider tokenService;
@Autowired
UserRepository userRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
var token = this.recoverToken(request);
if (token != null) {
var login = tokenService.validateToken(token);
var user = userRepository.findByLogin(login);
var authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String recoverToken(HttpServletRequest request) {
var authHeader = request.getHeader("Authorization");
if (authHeader == null)
return null;
return authHeader.replace("Bearer ", "");
}
}
在该方法中,doFilterInternal
我们从请求中恢复令牌,使用辅助方法从字符串中删除“Bearer” recoverToken
,验证令牌并在 中设置身份验证SecurityContextHolder
。这SecurityContextHolder
是一个 Spring Security 类,用于保存当前请求的身份验证,因此我们可以在控制器中访问用户信息。
授权配置
这里我们需要定义一些必要的方法来使我们的身份验证系统正常工作。顶部有Configuration
和@EnableWebSecurity
注解,用于在应用程序中启用 Web 安全性。然后我们定义SecurityFilterChain
Bean 来定义将受身份验证系统保护的端点。
// config/AuthConfig.java
@Configuration
@EnableWebSecurity
public class AuthConfig {
@Autowired
SecurityFilter securityFilter;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.POST, "/api/v1/auth/*").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/books").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在该方法中,authorizeHttpRequests
我们定义了需要保护的端点以及可以访问每个端点的角色。在我们的例子中,这些/api/v1/auth/*
端点是公开的,一个/api/v1/books
端点将受到保护,只有拥有相应角色的用户ADMIN
才能访问。其他端点将受到保护,只有经过身份验证的用户才能访问。
在这个方法中,addFilterBefore
我们定义了之前创建的过滤器。最后,我们定义了身份验证系统运行所需的AuthenticationManager
和bean。PasswordEncoder
授权DTO
我们需要两个 DTO 来接收用户凭证,以及另一个 DTO 在用户登录时返回令牌。
// dtos/SignUpDto.java
public record SignUpDto(
String login,
String password,
UserRole role) {
}
// dtos/SignInDto.java
public record SignInDto(
String login,
String password) {
}
// dtos/JwtDto.java
public record JwtDto(
String accessToken) {
}
授权服务
UserDetailsService
这里我们定义负责创建用户并将其保存在数据库中或通过用户名加载用户信息的服务实现。
// services/AuthService.java
@Service
public class AuthService implements UserDetailsService {
@Autowired
UserRepository repository;
@Override
public UserDetails loadUserByUsername(String username) {
var user = repository.findByLogin(username);
return user;
}
public UserDetails signUp(SignUpDto data) throws InvalidJwtException {
if (repository.findByLogin(data.login()) != null) {
throw new InvalidJwtException("Username already exists");
}
String encryptedPassword = new BCryptPasswordEncoder().encode(data.password());
User newUser = new User(data.login(), encryptedPassword, data.role());
return repository.save(newUser);
}
}
在该signUp
方法中,我们检查用户名是否已经注册,然后使用加密密码BCryptPasswordEncoder
并保存用户信息。
授权控制器
最后我们定义身份验证控制器。它将负责接收请求、验证用户身份并生成令牌。
// controllers/AuthController.java
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthService service;
@Autowired
private TokenProvider tokenService;
@PostMapping("/signup")
public ResponseEntity<?> signUp(@RequestBody @Valid SignUpDto data) {
service.signUp(data);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PostMapping("/signin")
public ResponseEntity<JwtDto> signIn(@RequestBody @Valid SignInDto data) {
var usernamePassword = new UsernamePasswordAuthenticationToken(data.login(), data.password());
var authUser = authenticationManager.authenticate(usernamePassword);
var accessToken = tokenService.generateAccessToken((User) authUser.getPrincipal());
return ResponseEntity.ok(new JwtDto(accessToken));
}
}
在该方法中,signUp
我们接收用户数据,创建新用户并将其保存到数据库中。在该signIn
方法中,我们接收用户凭证,使用 进行用户身份验证AuthenticationManager
,并生成令牌。
测试身份验证
要创建新用户,我们POST
向端点发送一个请求/api/v1/auth/signup
,其主体包含登录名、密码和可用角色之一(USER 或 ADMIN):
{
"login": "myusername",
"password": "123456",
"role": "USER"
}
为了检索身份验证令牌,我们POST
向端点发送包含此用户登录名和密码的请求/api/v1/auth/signin
。
为了测试我们的身份验证系统,我们将创建一个具有两个端点的简单书籍控制器,一个用于创建一本新书,另一个用于列出所有书籍。
@RestController
@RequestMapping("/api/v1/books")
public class BookController {
@GetMapping
public ResponseEntity<List<String>> findAll() {
return ResponseEntity.ok(List.of("Book1", "Book2", "Book3"));
}
@PostMapping
public ResponseEntity<String> create(@RequestBody String data) {
return ResponseEntity.ok(data);
}
}
在端点中,/api/v1/books
该GET
方法将可供具有角色的用户使用USER
,并且该POST
方法将受到保护,并且只有具有该角色的用户ADMIN
才能创建书籍。
呼!信息量和代码量都有点大,但我希望你喜欢,并且学到了新东西!如果你有任何问题或建议,欢迎在Twitter/X上给我留言。
感谢阅读!
文章来源:https://dev.to/m1guelsb/authentication-and-authorization-with-spring-boot-4m2n