feat: implement user credential management with new UserCredential entity and related components

This commit is contained in:
siujamo
2026-03-12 13:56:06 +08:00
parent 0562c8548b
commit 7ce241cc16
16 changed files with 229 additions and 137 deletions
+7 -7
View File
@@ -34,12 +34,12 @@
--- Type Definitions --- --- Type Definitions ---
DROP TYPE IF EXISTS USER_STATUS CASCADE; DROP TYPE IF EXISTS USER_STATUS CASCADE;
DROP TYPE IF EXISTS STATUS CASCADE; DROP TYPE IF EXISTS STATUS CASCADE;
DROP TYPE IF EXISTS IDENTITY_PROVIDER CASCADE; DROP TYPE IF EXISTS CREDENTIAL_PROVIDER CASCADE;
DROP TYPE IF EXISTS SETTING_TYPE CASCADE; DROP TYPE IF EXISTS SETTING_TYPE CASCADE;
CREATE TYPE USER_STATUS AS ENUM ('ACTIVE', 'INACTIVE', 'LOCKED'); CREATE TYPE USER_STATUS AS ENUM ('ACTIVE', 'INACTIVE', 'LOCKED');
CREATE TYPE STATUS AS ENUM ('ACTIVE', 'INACTIVE'); CREATE TYPE STATUS AS ENUM ('ACTIVE', 'INACTIVE');
CREATE TYPE IDENTITY_PROVIDER AS ENUM ('LOCAL', 'OIDC', 'MICROSOFT_ENTRA_ID', 'GOOGLE_OIDC', 'SAML'); CREATE TYPE CREDENTIAL_PROVIDER AS ENUM ('LOCAL', 'OIDC', 'MICROSOFT_ENTRA_ID', 'GOOGLE_OIDC', 'SAML');
CREATE TYPE SETTING_TYPE AS ENUM ('STRING', 'BOOLEAN', 'INT'); CREATE TYPE SETTING_TYPE AS ENUM ('STRING', 'BOOLEAN', 'INT');
--- Departments Table --- --- Departments Table ---
@@ -186,15 +186,15 @@ SELECT 'johndoe',
CURRENT_TIMESTAMP; CURRENT_TIMESTAMP;
--- User Identities Table --- --- User Identities Table ---
DROP TABLE IF EXISTS user_identities CASCADE; DROP TABLE IF EXISTS user_credentials CASCADE;
CREATE TABLE user_identities CREATE TABLE user_credentials
( (
user_id BIGINT NOT NULL REFERENCES users (id), user_id BIGINT NOT NULL REFERENCES users (id),
provider IDENTITY_PROVIDER NOT NULL, provider CREDENTIAL_PROVIDER NOT NULL,
external_id VARCHAR(255) NOT NULL, credential VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, provider, external_id) PRIMARY KEY (user_id, provider, credential)
); );
--- Roles Table --- --- Roles Table ---
@@ -48,12 +48,6 @@ public class User {
@Column(nullable = false, length = 64) @Column(nullable = false, length = 64)
private String username; private String username;
/**
* The encrypted password for user authentication.
*/
@Column
private String password;
/** /**
* The user's complete full name. * The user's complete full name.
*/ */
@@ -124,10 +118,9 @@ public class User {
public User() { public User() {
} }
public User(Long id, String username, String password, String fullName, String email, String regionAbbreviation, String phoneNumber, String avatarUrl, UserStatus status, Long departmentId, Long positionId, LocalDateTime createdAt, LocalDateTime updatedAt) { public User(Long id, String username, String fullName, String email, String regionAbbreviation, String phoneNumber, String avatarUrl, UserStatus status, Long departmentId, Long positionId, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id; this.id = id;
this.username = username; this.username = username;
this.password = password;
this.fullName = fullName; this.fullName = fullName;
this.email = email; this.email = email;
this.regionAbbreviation = regionAbbreviation; this.regionAbbreviation = regionAbbreviation;
@@ -176,24 +169,6 @@ public class User {
this.username = username; this.username = username;
} }
/**
* Gets the encrypted password.
*
* @return the encrypted password
*/
public String getPassword() {
return password;
}
/**
* Sets the encrypted password.
*
* @param password the encrypted password (never plain text)
*/
public void setPassword(String password) {
this.password = password;
}
public String getFullName() { public String getFullName() {
return fullName; return fullName;
} }
@@ -337,7 +312,6 @@ public class User {
public static class UserBuilder { public static class UserBuilder {
private Long id; private Long id;
private String username; private String username;
private String password;
private String fullName; private String fullName;
private String email; private String email;
private String regionAbbreviation; private String regionAbbreviation;
@@ -362,11 +336,6 @@ public class User {
return this; return this;
} }
public UserBuilder password(String password) {
this.password = password;
return this;
}
public UserBuilder fullName(String fullName) { public UserBuilder fullName(String fullName) {
this.fullName = fullName; this.fullName = fullName;
return this; return this;
@@ -423,7 +392,7 @@ public class User {
* @return a new User instance * @return a new User instance
*/ */
public User build() { public User build() {
return new User(id, username, password, fullName, email, regionAbbreviation, phoneNumber, avatarUrl, status, departmentId, positionId, createdAt, updatedAt); return new User(id, username, fullName, email, regionAbbreviation, phoneNumber, avatarUrl, status, departmentId, positionId, createdAt, updatedAt);
} }
} }
@@ -1,7 +1,7 @@
package com.onixbyte.helix.domain.entity; package com.onixbyte.helix.domain.entity;
import com.onixbyte.helix.enumeration.IdentityProvider; import com.onixbyte.helix.enumeration.CredentialProvider;
import com.onixbyte.helix.domain.entity.embeddable.UserIdentityId; import com.onixbyte.helix.domain.entity.embeddable.UserCredentialId;
import jakarta.persistence.*; // 导入 Jakarta Persistence API import jakarta.persistence.*; // 导入 Jakarta Persistence API
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Objects; import java.util.Objects;
@@ -19,14 +19,14 @@ import java.util.Objects;
* @since 1.0 * @since 1.0
*/ */
@Entity @Entity
@Table(name = "user_identities") @Table(name = "user_credentials")
public class UserIdentity { public class UserCredential {
/** /**
* The composite primary key for the entity, composed of userId, provider, and externalId. * The composite primary key for the entity, composed of userId, provider, and externalId.
*/ */
@EmbeddedId @EmbeddedId
private UserIdentityId id; private UserCredentialId id;
/** /**
* The timestamp when this identity mapping was created. * The timestamp when this identity mapping was created.
@@ -53,7 +53,7 @@ public class UserIdentity {
* @param userId the user ID * @param userId the user ID
*/ */
public void setUserId(Long userId) { public void setUserId(Long userId) {
if (this.id == null) this.id = new UserIdentityId(); if (this.id == null) this.id = new UserCredentialId();
this.id.setUserId(userId); this.id.setUserId(userId);
} }
@@ -61,7 +61,7 @@ public class UserIdentity {
* Gets the external identity provider from the composite primary key. * Gets the external identity provider from the composite primary key.
* @return the provider * @return the provider
*/ */
public IdentityProvider getProvider() { public CredentialProvider getProvider() {
return this.id != null ? this.id.getProvider() : null; return this.id != null ? this.id.getProvider() : null;
} }
@@ -69,8 +69,8 @@ public class UserIdentity {
* Sets the external identity provider within the composite primary key. * Sets the external identity provider within the composite primary key.
* @param provider the provider * @param provider the provider
*/ */
public void setProvider(IdentityProvider provider) { public void setProvider(CredentialProvider provider) {
if (this.id == null) this.id = new UserIdentityId(); if (this.id == null) this.id = new UserCredentialId();
this.id.setProvider(provider); this.id.setProvider(provider);
} }
@@ -78,17 +78,17 @@ public class UserIdentity {
* Gets the unique identifier from the external provider from the composite primary key. * Gets the unique identifier from the external provider from the composite primary key.
* @return the external ID * @return the external ID
*/ */
public String getExternalId() { public String getCredential() {
return this.id != null ? this.id.getExternalId() : null; return this.id != null ? this.id.getCredential() : null;
} }
/** /**
* Sets the unique identifier from the external provider within the composite primary key. * Sets the unique identifier from the external provider within the composite primary key.
* @param externalId the external ID * @param credential the external ID
*/ */
public void setExternalId(String externalId) { public void setCredential(String credential) {
if (this.id == null) this.id = new UserIdentityId(); if (this.id == null) this.id = new UserCredentialId();
this.id.setExternalId(externalId); this.id.setCredential(credential);
} }
public LocalDateTime getCreatedAt() { public LocalDateTime getCreatedAt() {
@@ -109,12 +109,12 @@ public class UserIdentity {
// --- Constructors (Adjusted for EmbeddedId) --- // --- Constructors (Adjusted for EmbeddedId) ---
public UserIdentity() { public UserCredential() {
this.id = new UserIdentityId(); // Initialize ID object for safety this.id = new UserCredentialId(); // Initialize ID object for safety
} }
public UserIdentity(Long userId, IdentityProvider provider, String externalId, LocalDateTime createdAt, LocalDateTime updatedAt) { public UserCredential(Long userId, CredentialProvider provider, String externalId, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = new UserIdentityId(userId, provider, externalId); this.id = new UserCredentialId(userId, provider, externalId);
this.createdAt = createdAt; this.createdAt = createdAt;
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
} }
@@ -125,7 +125,7 @@ public class UserIdentity {
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
UserIdentity that = (UserIdentity) o; UserCredential that = (UserCredential) o;
return Objects.equals(id, that.id); // Entity equality based on primary key return Objects.equals(id, that.id); // Entity equality based on primary key
} }
@@ -139,7 +139,7 @@ public class UserIdentity {
return "UserIdentity{" + return "UserIdentity{" +
"userId=" + getUserId() + "userId=" + getUserId() +
", provider=" + getProvider() + ", provider=" + getProvider() +
", externalId='" + getExternalId() + '\'' + ", externalId='" + getCredential() + '\'' +
", createdAt=" + createdAt + ", createdAt=" + createdAt +
", updatedAt=" + updatedAt + ", updatedAt=" + updatedAt +
'}'; '}';
@@ -162,7 +162,7 @@ public class UserIdentity {
*/ */
public static class UserIdentityBuilder { public static class UserIdentityBuilder {
private Long userId; private Long userId;
private IdentityProvider provider; private CredentialProvider provider;
private String externalId; private String externalId;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
@@ -175,7 +175,7 @@ public class UserIdentity {
return this; return this;
} }
public UserIdentityBuilder provider(IdentityProvider provider) { public UserIdentityBuilder provider(CredentialProvider provider) {
this.provider = provider; this.provider = provider;
return this; return this;
} }
@@ -200,8 +200,8 @@ public class UserIdentity {
* *
* @return a new UserIdentity instance * @return a new UserIdentity instance
*/ */
public UserIdentity build() { public UserCredential build() {
return new UserIdentity(userId, provider, externalId, createdAt, updatedAt); return new UserCredential(userId, provider, externalId, createdAt, updatedAt);
} }
} }
@@ -1,6 +1,6 @@
package com.onixbyte.helix.domain.entity.embeddable; package com.onixbyte.helix.domain.entity.embeddable;
import com.onixbyte.helix.enumeration.IdentityProvider; import com.onixbyte.helix.enumeration.CredentialProvider;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Embeddable; import jakarta.persistence.Embeddable;
import jakarta.persistence.Enumerated; import jakarta.persistence.Enumerated;
@@ -18,7 +18,7 @@ import java.util.Objects;
* from that provider. * from that provider.
*/ */
@Embeddable @Embeddable
public class UserIdentityId implements Serializable { public class UserCredentialId implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -37,23 +37,23 @@ public class UserIdentityId implements Serializable {
@Column(nullable = false) @Column(nullable = false)
@Enumerated @Enumerated
@JdbcType(PostgreSQLEnumJdbcType.class) @JdbcType(PostgreSQLEnumJdbcType.class)
private IdentityProvider provider; private CredentialProvider provider;
/** /**
* The unique identifier from the external provider, corresponding to the 'external_id' column. * The unique identifier from the external provider, corresponding to the 'external_id' column.
*/ */
@Column(nullable = false) @Column(nullable = false)
private String externalId; private String credential;
// --- Constructors --- // --- Constructors ---
public UserIdentityId() { public UserCredentialId() {
} }
public UserIdentityId(Long userId, IdentityProvider provider, String externalId) { public UserCredentialId(Long userId, CredentialProvider provider, String credential) {
this.userId = userId; this.userId = userId;
this.provider = provider; this.provider = provider;
this.externalId = externalId; this.credential = credential;
} }
// --- Getters and Setters (Omitted for brevity, but should exist) --- // --- Getters and Setters (Omitted for brevity, but should exist) ---
@@ -65,32 +65,32 @@ public class UserIdentityId implements Serializable {
this.userId = userId; this.userId = userId;
} }
public IdentityProvider getProvider() { public CredentialProvider getProvider() {
return provider; return provider;
} }
public void setProvider(IdentityProvider provider) { public void setProvider(CredentialProvider provider) {
this.provider = provider; this.provider = provider;
} }
public String getExternalId() { public String getCredential() {
return externalId; return credential;
} }
public void setExternalId(String externalId) { public void setCredential(String credential) {
this.externalId = externalId; this.credential = credential;
} }
// --- equals and hashCode (REQUIRED for composite keys) --- // --- equals and hashCode (REQUIRED for composite keys) ---
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
UserIdentityId that = (UserIdentityId) o; UserCredentialId that = (UserCredentialId) o;
return Objects.equals(userId, that.userId) && provider == that.provider && Objects.equals(externalId, that.externalId); return Objects.equals(userId, that.userId) && provider == that.provider && Objects.equals(credential, that.credential);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(userId, provider, externalId); return Objects.hash(userId, provider, credential);
} }
} }
@@ -1,7 +1,7 @@
package com.onixbyte.helix.domain.view; package com.onixbyte.helix.domain.view;
import com.onixbyte.helix.enumeration.IdentityProvider; import com.onixbyte.helix.enumeration.CredentialProvider;
import com.onixbyte.helix.domain.entity.UserIdentity; import com.onixbyte.helix.domain.entity.UserCredential;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Objects; import java.util.Objects;
@@ -26,7 +26,7 @@ public class UserIdentityView {
/** /**
* The external identity provider. * The external identity provider.
*/ */
private IdentityProvider provider; private CredentialProvider provider;
/** /**
* The unique identifier from the external provider. * The unique identifier from the external provider.
@@ -52,7 +52,7 @@ public class UserIdentityView {
/** /**
* Constructor with all fields. * Constructor with all fields.
*/ */
public UserIdentityView(Long userId, IdentityProvider provider, String externalId, public UserIdentityView(Long userId, CredentialProvider provider, String externalId,
LocalDateTime createdAt, LocalDateTime updatedAt) { LocalDateTime createdAt, LocalDateTime updatedAt) {
this.userId = userId; this.userId = userId;
this.provider = provider; this.provider = provider;
@@ -64,19 +64,19 @@ public class UserIdentityView {
/** /**
* Creates a UserIdentityView from a UserIdentity entity. * Creates a UserIdentityView from a UserIdentity entity.
* *
* @param userIdentity the UserIdentity entity * @param userCredential the UserIdentity entity
* @return the UserIdentityView object * @return the UserIdentityView object
*/ */
public static UserIdentityView fromEntity(UserIdentity userIdentity) { public static UserIdentityView fromEntity(UserCredential userCredential) {
if (userIdentity == null) { if (userCredential == null) {
return null; return null;
} }
return new UserIdentityView( return new UserIdentityView(
userIdentity.getUserId(), userCredential.getUserId(),
userIdentity.getProvider(), userCredential.getProvider(),
userIdentity.getExternalId(), userCredential.getCredential(),
userIdentity.getCreatedAt(), userCredential.getCreatedAt(),
userIdentity.getUpdatedAt() userCredential.getUpdatedAt()
); );
} }
@@ -88,11 +88,11 @@ public class UserIdentityView {
this.userId = userId; this.userId = userId;
} }
public IdentityProvider getProvider() { public CredentialProvider getProvider() {
return provider; return provider;
} }
public void setProvider(IdentityProvider provider) { public void setProvider(CredentialProvider provider) {
this.provider = provider; this.provider = provider;
} }
@@ -162,7 +162,7 @@ public class UserIdentityView {
*/ */
public static class UserIdentityViewBuilder { public static class UserIdentityViewBuilder {
private Long userId; private Long userId;
private IdentityProvider provider; private CredentialProvider provider;
private String externalId; private String externalId;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
@@ -175,7 +175,7 @@ public class UserIdentityView {
return this; return this;
} }
public UserIdentityViewBuilder provider(IdentityProvider provider) { public UserIdentityViewBuilder provider(CredentialProvider provider) {
this.provider = provider; this.provider = provider;
return this; return this;
} }
@@ -6,8 +6,4 @@ public record LoginSuccessResponse(
String accessToken, String accessToken,
User user User user
) { ) {
public LoginSuccessResponse {
user.setPassword(null);
}
} }
@@ -1,3 +1,25 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.helix.enumeration; package com.onixbyte.helix.enumeration;
import com.onixbyte.helix.config.AuthenticationConfig; import com.onixbyte.helix.config.AuthenticationConfig;
@@ -18,7 +40,7 @@ import com.onixbyte.helix.config.AuthenticationConfig;
* @since 1.0.0 * @since 1.0.0
* @see AuthenticationConfig * @see AuthenticationConfig
*/ */
public enum IdentityProvider { public enum CredentialProvider {
/** /**
* Local identity provider using the application's internal user database. * Local identity provider using the application's internal user database.
@@ -67,8 +67,6 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
.map((authority) -> (GrantedAuthority) authority::getCode) .map((authority) -> (GrantedAuthority) authority::getCode)
.toList(); .toList();
user.setPassword(null);
var authentication = UsernamePasswordAuthentication.authenticated(user, authorities); var authentication = UsernamePasswordAuthentication.authenticated(user, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
@@ -1,6 +1,8 @@
package com.onixbyte.helix.manager; package com.onixbyte.helix.manager;
import com.onixbyte.helix.common.regex.Patterns; import com.onixbyte.helix.common.regex.Patterns;
import com.onixbyte.helix.domain.web.request.ResetPasswordRequest;
import com.onixbyte.helix.mapper.UserCredentialMapper;
import com.onixbyte.helix.shared.CacheName; import com.onixbyte.helix.shared.CacheName;
import com.onixbyte.helix.domain.database.query.wrapper.QueryUserWrapper; import com.onixbyte.helix.domain.database.query.wrapper.QueryUserWrapper;
import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.domain.entity.User;
@@ -30,16 +32,19 @@ public class UserManager {
private final UserMapper userMapper; private final UserMapper userMapper;
private final UserRepository userRepository; private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final UserCredentialMapper userCredentialMapper;
@Autowired @Autowired
public UserManager( public UserManager(
UserMapper userMapper, UserMapper userMapper,
UserRepository userRepository, UserRepository userRepository,
PasswordEncoder passwordEncoder PasswordEncoder passwordEncoder,
UserCredentialMapper userCredentialMapper
) { ) {
this.userMapper = userMapper; this.userMapper = userMapper;
this.userRepository = userRepository; this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.userCredentialMapper = userCredentialMapper;
} }
/** /**
@@ -87,11 +92,6 @@ public class UserManager {
var userToUpdate = userRepository.findById(user.getId()) var userToUpdate = userRepository.findById(user.getId())
.orElseThrow(() -> new BizException(HttpStatus.BAD_REQUEST, "找不到 ID 为" + user.getId() + "的用户信息")); .orElseThrow(() -> new BizException(HttpStatus.BAD_REQUEST, "找不到 ID 为" + user.getId() + "的用户信息"));
Optional.ofNullable(user.getPassword())
.filter(StringUtils::isNotBlank)
.map(passwordEncoder::encode)
.ifPresent(userToUpdate::setPassword);
Optional.ofNullable(user.getFullName()) Optional.ofNullable(user.getFullName())
.filter(StringUtils::isNotBlank) .filter(StringUtils::isNotBlank)
.ifPresent(userToUpdate::setFullName); .ifPresent(userToUpdate::setFullName);
@@ -126,4 +126,12 @@ public class UserManager {
return userToUpdate; return userToUpdate;
} }
@Transactional(rollbackFor = Throwable.class)
public void updateUserPassword(ResetPasswordRequest request) {
userCredentialMapper.updateUserCredential(
request.id(),
passwordEncoder.encode(request.password())
);
}
} }
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.helix.mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface UserCredentialMapper {
int updateUserCredential(
@Param("userId") Long userId,
@Param("encodedPassword") String encodedPassword
);
}
@@ -0,0 +1,10 @@
package com.onixbyte.helix.repository;
import com.onixbyte.helix.domain.entity.UserCredential;
import com.onixbyte.helix.domain.entity.embeddable.UserCredentialId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserCredentialRepository extends JpaRepository<UserCredential, UserCredentialId> {
}
@@ -1,10 +0,0 @@
package com.onixbyte.helix.repository;
import com.onixbyte.helix.domain.entity.UserIdentity;
import com.onixbyte.helix.domain.entity.embeddable.UserIdentityId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserIdentityRepository extends JpaRepository<UserIdentity, UserIdentityId> {
}
@@ -1,13 +1,17 @@
package com.onixbyte.helix.security.provider; package com.onixbyte.helix.security.provider;
import com.onixbyte.helix.domain.entity.Authority; import com.onixbyte.helix.domain.entity.Authority;
import com.onixbyte.helix.domain.entity.UserCredential;
import com.onixbyte.helix.enumeration.CredentialProvider;
import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.exception.BizException;
import com.onixbyte.helix.manager.AuthorityManager; import com.onixbyte.helix.manager.AuthorityManager;
import com.onixbyte.helix.manager.UserManager; import com.onixbyte.helix.manager.UserManager;
import com.onixbyte.helix.repository.UserCredentialRepository;
import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -24,16 +28,19 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro
private final UserManager userManager; private final UserManager userManager;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final AuthorityManager authorityManager; private final AuthorityManager authorityManager;
private final UserCredentialRepository userCredentialRepository;
@Autowired @Autowired
public UsernamePasswordAuthenticationProvider( public UsernamePasswordAuthenticationProvider(
UserManager userManager, UserManager userManager,
PasswordEncoder passwordEncoder, PasswordEncoder passwordEncoder,
AuthorityManager authorityManager AuthorityManager authorityManager,
UserCredentialRepository userCredentialRepository
) { ) {
this.userManager = userManager; this.userManager = userManager;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.authorityManager = authorityManager; this.authorityManager = authorityManager;
this.userCredentialRepository = userCredentialRepository;
} }
@Override @Override
@@ -49,14 +56,20 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro
throw new BizException(HttpStatus.UNAUTHORIZED, "用户名或密码错误。"); throw new BizException(HttpStatus.UNAUTHORIZED, "用户名或密码错误。");
} }
// get user credentials from database
var userCredentials = userCredentialRepository.findOne(Example.of(UserCredential.builder()
.provider(CredentialProvider.LOCAL)
.userId(user.getId())
.build()))
.orElseThrow(() -> new BizException(HttpStatus.UNAUTHORIZED, "您还没有配置密码,请使用第三方账号登录"));
// validate password // validate password
if (!passwordEncoder.matches(usernamePasswordAuthentication.getCredentials(), user.getPassword())) { if (!passwordEncoder.matches(usernamePasswordAuthentication.getCredentials(), userCredentials.getCredential())) {
log.error("User {} is trying to authenticate but password is incorrect.", usernamePasswordAuthentication.getPrincipal()); log.error("User {} is trying to authenticate but password is incorrect.", usernamePasswordAuthentication.getPrincipal());
throw new BizException(HttpStatus.UNAUTHORIZED, "用户名或密码错误。"); throw new BizException(HttpStatus.UNAUTHORIZED, "用户名或密码错误。");
} }
// erase credentials // erase credentials
user.setPassword(null);
usernamePasswordAuthentication.eraseCredentials(); usernamePasswordAuthentication.eraseCredentials();
// get authorities // get authorities
@@ -3,7 +3,9 @@ package com.onixbyte.helix.service;
import com.onixbyte.helix.domain.entity.Setting; import com.onixbyte.helix.domain.entity.Setting;
import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.domain.entity.User;
import com.onixbyte.helix.domain.web.request.LoginRequest; import com.onixbyte.helix.domain.web.request.LoginRequest;
import com.onixbyte.helix.enumeration.ApplicationMode;
import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.exception.BizException;
import com.onixbyte.helix.manager.ApplicationManager;
import com.onixbyte.helix.manager.CaptchaManager; import com.onixbyte.helix.manager.CaptchaManager;
import com.onixbyte.helix.manager.SecurityManager; import com.onixbyte.helix.manager.SecurityManager;
import com.onixbyte.helix.manager.SettingManager; import com.onixbyte.helix.manager.SettingManager;
@@ -30,18 +32,20 @@ public class AuthService {
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
private final SettingManager settingManager; private final SettingManager settingManager;
private final SecurityManager securityManager; private final SecurityManager securityManager;
private final ApplicationManager applicationManager;
@Autowired @Autowired
public AuthService( public AuthService(
CaptchaManager captchaManager, CaptchaManager captchaManager,
AuthenticationManager authenticationManager, AuthenticationManager authenticationManager,
SettingManager settingManager, SettingManager settingManager,
SecurityManager securityManager SecurityManager securityManager,
) { ApplicationManager applicationManager) {
this.captchaManager = captchaManager; this.captchaManager = captchaManager;
this.authenticationManager = authenticationManager; this.authenticationManager = authenticationManager;
this.settingManager = settingManager; this.settingManager = settingManager;
this.securityManager = securityManager; this.securityManager = securityManager;
this.applicationManager = applicationManager;
} }
/** /**
@@ -94,10 +98,22 @@ public class AuthService {
public ResponseCookie buildCookie(String cookieName, String token) { public ResponseCookie buildCookie(String cookieName, String token) {
var cookieBuilder = ResponseCookie.from(cookieName, token) var cookieBuilder = ResponseCookie.from(cookieName, token)
.httpOnly(true)
.maxAge(securityManager.getTokenValidDuration()) .maxAge(securityManager.getTokenValidDuration())
.secure(true)
.path("/"); .path("/");
var applicationMode = applicationManager.getApplicationMode();
switch (applicationMode) {
case PRODUCTION -> {
cookieBuilder.httpOnly(true);
}
case DEVELOPMENT -> {
cookieBuilder.sameSite("NONE");
}
case null, default -> {
}
}
return cookieBuilder.build(); return cookieBuilder.build();
} }
@@ -12,6 +12,8 @@ import com.onixbyte.helix.domain.web.request.ResetPasswordRequest;
import com.onixbyte.helix.domain.web.request.EditUserRequest; import com.onixbyte.helix.domain.web.request.EditUserRequest;
import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.domain.web.response.UserDetailResponse;
import com.onixbyte.helix.manager.*; import com.onixbyte.helix.manager.*;
import com.onixbyte.helix.mapper.UserCredentialMapper;
import com.onixbyte.helix.repository.UserCredentialRepository;
import com.onixbyte.identitygenerator.IdentityGenerator; import com.onixbyte.identitygenerator.IdentityGenerator;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -35,6 +37,8 @@ public class UserService {
private final ApplicationManager applicationManager; private final ApplicationManager applicationManager;
private final DepartmentManager departmentManager; private final DepartmentManager departmentManager;
private final PositionManager positionManager; private final PositionManager positionManager;
private final UserCredentialRepository userCredentialRepository;
private final UserCredentialMapper userCredentialMapper;
@Autowired @Autowired
public UserService( public UserService(
@@ -45,8 +49,8 @@ public class UserService {
PasswordEncoder passwordEncoder, PasswordEncoder passwordEncoder,
ApplicationManager applicationManager, ApplicationManager applicationManager,
DepartmentManager departmentManager, DepartmentManager departmentManager,
PositionManager positionManager PositionManager positionManager,
) { UserCredentialRepository userCredentialRepository, UserCredentialMapper userCredentialMapper) {
this.userManager = userManager; this.userManager = userManager;
this.userIdentityGenerator = userIdentityGenerator; this.userIdentityGenerator = userIdentityGenerator;
this.roleManager = roleManager; this.roleManager = roleManager;
@@ -55,6 +59,8 @@ public class UserService {
this.applicationManager = applicationManager; this.applicationManager = applicationManager;
this.departmentManager = departmentManager; this.departmentManager = departmentManager;
this.positionManager = positionManager; this.positionManager = positionManager;
this.userCredentialRepository = userCredentialRepository;
this.userCredentialMapper = userCredentialMapper;
} }
public Page<UserDetailResponse> queryUserDetailsPage(Pageable pageable, QueryUserRequest request) { public Page<UserDetailResponse> queryUserDetailsPage(Pageable pageable, QueryUserRequest request) {
@@ -98,7 +104,6 @@ public class UserService {
var user = userManager.save(User.builder() var user = userManager.save(User.builder()
.id(userIdentityGenerator.nextId()) .id(userIdentityGenerator.nextId())
.username(request.username()) .username(request.username())
.password(passwordEncoder.encode(request.password()))
.fullName(request.fullName()) .fullName(request.fullName())
.email(request.email()) .email(request.email())
.regionAbbreviation(request.regionAbbreviation()) .regionAbbreviation(request.regionAbbreviation())
@@ -168,10 +173,7 @@ public class UserService {
@Transactional(rollbackFor = Throwable.class) @Transactional(rollbackFor = Throwable.class)
public void resetPassword(ResetPasswordRequest request) { public void resetPassword(ResetPasswordRequest request) {
userManager.updateUser(User.builder() userManager.updateUserPassword(request);
.id(request.id())
.password(request.password())
.build());
} }
@Transactional(rollbackFor = Throwable.class) @Transactional(rollbackFor = Throwable.class)
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
~ Copyright (c) 2024-2026 OnixByte
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.onixbyte.helix.mapper.UserCredentialMapper">
<update id="updateUserCredential">
UPDATE user_credentials
SET credential = #{encodedPassword}
WHERE user_id = #{userId}
AND provider = 'LOCAL'::credential_provider
</update>
</mapper>