From 8ed7ab5d40622e8ae09e7e34547f31ed2fadaaa2 Mon Sep 17 00:00:00 2001 From: jianggang Date: Wed, 31 Aug 2022 18:00:30 +0800 Subject: [PATCH] feat: Add operation logs (#30) * feat: add operation logs --- .../GuestAuthenticationProcessingFilter.java | 5 +- .../api/auth/GuestAuthenticationProvider.java | 11 +++- .../api/auth/GuestAuthenticationToken.java | 10 +++- ...asswordAuthenticationProcessingFilter.java | 5 +- .../UserPasswordAuthenticationProvider.java | 8 +++ .../auth/UserPasswordAuthenticationToken.java | 10 +++- .../api/base/enums/OperationType.java | 5 ++ .../api/dto/MemberCreateRequest.java | 2 + .../com/featureprobe/api/entity/Member.java | 2 + .../featureprobe/api/entity/OperationLog.java | 53 +++++++++++++++++++ .../repository/OperationLogRepository.java | 11 ++++ .../api/service/GuestService.java | 3 +- .../api/service/MemberService.java | 8 ++- .../api/service/OperationLogService.java | 20 +++++++ .../V29__add_member_source_fields.sql | 12 +++++ .../api/service/GuestServiceSpec.groovy | 2 +- .../service/OperationLogServiceSpec.groovy | 25 +++++++++ .../UserPasswordAuthenticationSpec.groovy | 12 ++++- 18 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/featureprobe/api/base/enums/OperationType.java create mode 100644 src/main/java/com/featureprobe/api/entity/OperationLog.java create mode 100644 src/main/java/com/featureprobe/api/repository/OperationLogRepository.java create mode 100644 src/main/java/com/featureprobe/api/service/OperationLogService.java create mode 100644 src/main/resources/db/migration/V29__add_member_source_fields.sql create mode 100644 src/test/groovy/com/featureprobe/api/service/OperationLogServiceSpec.groovy diff --git a/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProcessingFilter.java b/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProcessingFilter.java index 758ae3d..9c594d1 100644 --- a/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProcessingFilter.java +++ b/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProcessingFilter.java @@ -22,6 +22,8 @@ public class GuestAuthenticationProcessingFilter extends AbstractAuthenticationP private static final String GUEST_LOGIN_ACCOUNT_PARAM = "account"; + private static final String ACCOUNT_SOURCE = "source"; + protected GuestAuthenticationProcessingFilter() { super(GUEST_LOGIN_PATH); } @@ -34,7 +36,8 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ String body = IOUtils.toString(is, StandardCharsets.UTF_8); Map authParam = mapper.readValue(body, Map.class); String account = authParam.get(GUEST_LOGIN_ACCOUNT_PARAM); - return getAuthenticationManager().authenticate(new GuestAuthenticationToken(account, "")); + String source = authParam.get(ACCOUNT_SOURCE); + return getAuthenticationManager().authenticate(new GuestAuthenticationToken(account, source, "")); } } diff --git a/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProvider.java b/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProvider.java index c55f5ab..6e44bf7 100644 --- a/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProvider.java +++ b/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProvider.java @@ -1,8 +1,11 @@ package com.featureprobe.api.auth; +import com.featureprobe.api.base.enums.OperationType; import com.featureprobe.api.entity.Member; +import com.featureprobe.api.entity.OperationLog; import com.featureprobe.api.service.GuestService; import com.featureprobe.api.service.MemberService; +import com.featureprobe.api.service.OperationLogService; import lombok.AllArgsConstructor; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; @@ -21,16 +24,22 @@ public class GuestAuthenticationProvider implements AuthenticationProvider { private GuestService guestService; + private OperationLogService operationLogService; + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { GuestAuthenticationToken token = (GuestAuthenticationToken) authentication; Optional member = memberService.findByAccount(token.getAccount()); + OperationLog log = new OperationLog(OperationType.LOGIN.name() + "_" + token.getSource(), + token.getAccount()); if (member.isPresent()) { memberService.updateVisitedTime(token.getAccount()); + operationLogService.save(log); return new UserPasswordAuthenticationToken(member.get(), Arrays.asList(new SimpleGrantedAuthority(member.get().getRole().name()))); } else { - Member newMember = guestService.initGuest(token.getAccount()); + Member newMember = guestService.initGuest(token.getAccount(), token.getSource()); + operationLogService.save(log); return new UserPasswordAuthenticationToken(newMember, Arrays.asList(new SimpleGrantedAuthority(newMember.getRole().name()))); } diff --git a/src/main/java/com/featureprobe/api/auth/GuestAuthenticationToken.java b/src/main/java/com/featureprobe/api/auth/GuestAuthenticationToken.java index 9394248..2963dfc 100644 --- a/src/main/java/com/featureprobe/api/auth/GuestAuthenticationToken.java +++ b/src/main/java/com/featureprobe/api/auth/GuestAuthenticationToken.java @@ -10,13 +10,16 @@ public class GuestAuthenticationToken extends AbstractAuthenticationToken { private String account; + private String source; + private String password; private Member principal; - public GuestAuthenticationToken(String account, String password) { + public GuestAuthenticationToken(String account, String source, String password) { super(null); this.account = account; + this.source = source; this.password = password; super.setAuthenticated(false); } @@ -46,11 +49,14 @@ public String getAccount() { return account; } + public String getSource() { + return source; + } + public String getPassword() { return password; } - public String getRole() { if (principal == null) { return null; diff --git a/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProcessingFilter.java b/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProcessingFilter.java index b3ba40a..2110bce 100644 --- a/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProcessingFilter.java +++ b/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProcessingFilter.java @@ -21,6 +21,8 @@ public class UserPasswordAuthenticationProcessingFilter extends AbstractAuthenti private static final String GUEST_LOGIN_ACCOUNT_PARAM = "account"; + private static final String ACCOUNT_SOURCE = "source"; + private static final String GUEST_LOGIN_PASSWORD_PARAM = "password"; protected UserPasswordAuthenticationProcessingFilter() { @@ -35,7 +37,8 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ String body = IOUtils.toString(is, StandardCharsets.UTF_8); Map authParam = mapper.readValue(body, Map.class); String account = authParam.get(GUEST_LOGIN_ACCOUNT_PARAM); + String source = authParam.get(ACCOUNT_SOURCE); String password = authParam.get(GUEST_LOGIN_PASSWORD_PARAM); - return getAuthenticationManager().authenticate(new UserPasswordAuthenticationToken(account, password)); + return getAuthenticationManager().authenticate(new UserPasswordAuthenticationToken(account, source, password)); } } diff --git a/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProvider.java b/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProvider.java index d095764..47e2919 100644 --- a/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProvider.java +++ b/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProvider.java @@ -1,7 +1,10 @@ package com.featureprobe.api.auth; +import com.featureprobe.api.base.enums.OperationType; import com.featureprobe.api.entity.Member; +import com.featureprobe.api.entity.OperationLog; import com.featureprobe.api.service.MemberService; +import com.featureprobe.api.service.OperationLogService; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.security.authentication.AuthenticationProvider; @@ -19,14 +22,19 @@ public class UserPasswordAuthenticationProvider implements AuthenticationProvide private MemberService memberService; + private OperationLogService operationLogService; + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { UserPasswordAuthenticationToken token = (UserPasswordAuthenticationToken) authentication; if (StringUtils.isNotBlank(token.getAccount()) && StringUtils.isNotBlank(token.getPassword())) { Optional member = memberService.findByAccount(token.getAccount()); + OperationLog log = new OperationLog(OperationType.LOGIN.name() + "_" + token.getSource(), + token.getAccount()); if (member.isPresent() && new BCryptPasswordEncoder().matches(token.getPassword(), member.get().getPassword())) { memberService.updateVisitedTime(token.getAccount()); + operationLogService.save(log); return new UserPasswordAuthenticationToken(member.get(), Arrays.asList(new SimpleGrantedAuthority(member.get().getRole().name()))); } diff --git a/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationToken.java b/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationToken.java index 5fce603..864c602 100644 --- a/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationToken.java +++ b/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationToken.java @@ -10,13 +10,16 @@ public class UserPasswordAuthenticationToken extends AbstractAuthenticationToken private String account; + private String source; + private String password; private Member principal; - public UserPasswordAuthenticationToken(String account, String password) { + public UserPasswordAuthenticationToken(String account, String source, String password) { super(null); this.account = account; + this.source = source; this.password = password; super.setAuthenticated(false); } @@ -46,11 +49,14 @@ public String getAccount() { return account; } + public String getSource() { + return source; + } + public String getPassword() { return password; } - public String getRole() { if (principal == null) { return null; diff --git a/src/main/java/com/featureprobe/api/base/enums/OperationType.java b/src/main/java/com/featureprobe/api/base/enums/OperationType.java new file mode 100644 index 0000000..e0373c6 --- /dev/null +++ b/src/main/java/com/featureprobe/api/base/enums/OperationType.java @@ -0,0 +1,5 @@ +package com.featureprobe.api.base.enums; + +public enum OperationType { + LOGIN +} diff --git a/src/main/java/com/featureprobe/api/dto/MemberCreateRequest.java b/src/main/java/com/featureprobe/api/dto/MemberCreateRequest.java index fcc1aaa..e80b476 100644 --- a/src/main/java/com/featureprobe/api/dto/MemberCreateRequest.java +++ b/src/main/java/com/featureprobe/api/dto/MemberCreateRequest.java @@ -12,6 +12,8 @@ public class MemberCreateRequest { @NotNull private List accounts; + private String source; + @NotBlank private String password; diff --git a/src/main/java/com/featureprobe/api/entity/Member.java b/src/main/java/com/featureprobe/api/entity/Member.java index c56b137..db4a5ab 100644 --- a/src/main/java/com/featureprobe/api/entity/Member.java +++ b/src/main/java/com/featureprobe/api/entity/Member.java @@ -53,6 +53,8 @@ public class Member extends AbstractAuditEntity implements AuthenticatedPrincipa @Column(columnDefinition = "TINYINT") private Boolean deleted; + private String source; + @ManyToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY) @JoinTable(name = "organization_member", joinColumns = @JoinColumn(name = "member_id"), inverseJoinColumns = @JoinColumn(name = "organization_id")) diff --git a/src/main/java/com/featureprobe/api/entity/OperationLog.java b/src/main/java/com/featureprobe/api/entity/OperationLog.java new file mode 100644 index 0000000..54f6a66 --- /dev/null +++ b/src/main/java/com/featureprobe/api/entity/OperationLog.java @@ -0,0 +1,53 @@ +package com.featureprobe.api.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.hibernate.annotations.DynamicInsert; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; + +@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor +@Data +@Entity +@Table(name = "operation_logs") +@DynamicInsert +@ToString(callSuper = true) +public class OperationLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "res_param") + private String response; + + @Column(name = "req_param") + private String request; + + private String type; + + private String account; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_time") + private Date createdTime; + + public OperationLog(String type, String account) { + this.type = type; + this.account = account; + } + +} diff --git a/src/main/java/com/featureprobe/api/repository/OperationLogRepository.java b/src/main/java/com/featureprobe/api/repository/OperationLogRepository.java new file mode 100644 index 0000000..f222c5c --- /dev/null +++ b/src/main/java/com/featureprobe/api/repository/OperationLogRepository.java @@ -0,0 +1,11 @@ +package com.featureprobe.api.repository; + +import com.featureprobe.api.entity.OperationLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +@Repository +public interface OperationLogRepository extends JpaRepository, + JpaSpecificationExecutor { +} diff --git a/src/main/java/com/featureprobe/api/service/GuestService.java b/src/main/java/com/featureprobe/api/service/GuestService.java index aaff12c..08636b8 100644 --- a/src/main/java/com/featureprobe/api/service/GuestService.java +++ b/src/main/java/com/featureprobe/api/service/GuestService.java @@ -53,7 +53,7 @@ public class GuestService { private static final String DEMO_INIT_DATA_FILE_PATH = "db/demo_init_data.sql"; @Transactional(rollbackFor = Exception.class) - public Member initGuest(String account) { + public Member initGuest(String account, String source) { Member createdMember = new Member(); createdMember.setAccount(account); createdMember.setPassword(passwordEncoder.encode(appConfig.getGuestDefaultPassword())); @@ -61,6 +61,7 @@ public Member initGuest(String account) { List organizations = new ArrayList<>(1); organizations.add(new Organization(account)); createdMember.setOrganizations(organizations); + createdMember.setSource(source); Member savedMember = memberRepository.save(createdMember); SecurityContextHolder.setContext(new SecurityContextImpl(new JwtAuthenticationToken(Jwt.withTokenValue("_") .claim("userId", savedMember.getId()).claim("account", savedMember.getAccount()) diff --git a/src/main/java/com/featureprobe/api/service/MemberService.java b/src/main/java/com/featureprobe/api/service/MemberService.java index 2fac0d6..bdd7b2b 100644 --- a/src/main/java/com/featureprobe/api/service/MemberService.java +++ b/src/main/java/com/featureprobe/api/service/MemberService.java @@ -60,6 +60,8 @@ public class MemberService { @PersistenceContext public EntityManager entityManager; + private static final String API_CREATE_MEMBER_SOURCE = "INTERNAL"; + private static final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); @Transactional(rollbackFor = Exception.class) @@ -109,12 +111,14 @@ private List newNumbers(MemberCreateRequest createRequest) { return createRequest.getAccounts() .stream() .filter(account -> memberIncludeDeletedService.validateAccountIncludeDeleted(account)) - .map(account -> newMember(account, createRequest.getPassword())).collect(Collectors.toList()); + .map(account -> newMember(account, createRequest.getSource(), createRequest.getPassword())) + .collect(Collectors.toList()); } - private Member newMember(String account, String password) { + private Member newMember(String account, String source, String password) { Member member = new Member(); member.setAccount(account); + member.setSource(source); member.setRole(RoleEnum.MEMBER); member.setPassword(new BCryptPasswordEncoder().encode(password)); Organization organization = organizationRepository.findById(TenantContext.getCurrentOrganization() diff --git a/src/main/java/com/featureprobe/api/service/OperationLogService.java b/src/main/java/com/featureprobe/api/service/OperationLogService.java new file mode 100644 index 0000000..f811cda --- /dev/null +++ b/src/main/java/com/featureprobe/api/service/OperationLogService.java @@ -0,0 +1,20 @@ +package com.featureprobe.api.service; + +import com.featureprobe.api.entity.OperationLog; +import com.featureprobe.api.repository.OperationLogRepository; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@AllArgsConstructor +public class OperationLogService { + + private OperationLogRepository operationLogRepository; + + public void save(OperationLog log) { + operationLogRepository.save(log); + } + +} diff --git a/src/main/resources/db/migration/V29__add_member_source_fields.sql b/src/main/resources/db/migration/V29__add_member_source_fields.sql new file mode 100644 index 0000000..bbbf064 --- /dev/null +++ b/src/main/resources/db/migration/V29__add_member_source_fields.sql @@ -0,0 +1,12 @@ +alter table member add source VARCHAR(256) default '' not null after deleted; + +create table operation_logs +( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `res_param` text, + `req_param` text, + `type` varchar(64) NOT NULL DEFAULT '', + `account` varchar(128) NOT NULL DEFAULT '', + `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +)ENGINE=InnoDB collate = utf8mb4_unicode_ci; \ No newline at end of file diff --git a/src/test/groovy/com/featureprobe/api/service/GuestServiceSpec.groovy b/src/test/groovy/com/featureprobe/api/service/GuestServiceSpec.groovy index 7a33225..cc3e4c8 100644 --- a/src/test/groovy/com/featureprobe/api/service/GuestServiceSpec.groovy +++ b/src/test/groovy/com/featureprobe/api/service/GuestServiceSpec.groovy @@ -50,7 +50,7 @@ class GuestServiceSpec extends Specification{ given: Query query = Mock(NativeQueryImplementor) when: - def guest = guestService.initGuest("Admin") + def guest = guestService.initGuest("Admin", "test") then: 1 * memberRepository.save(_) >> new Member(id: 1, account: "Admin", role: RoleEnum.ADMIN, organizations: [new Organization(id: 1)]) 1 * projectRepository.count() >> 2 diff --git a/src/test/groovy/com/featureprobe/api/service/OperationLogServiceSpec.groovy b/src/test/groovy/com/featureprobe/api/service/OperationLogServiceSpec.groovy new file mode 100644 index 0000000..6f0b6ca --- /dev/null +++ b/src/test/groovy/com/featureprobe/api/service/OperationLogServiceSpec.groovy @@ -0,0 +1,25 @@ +package com.featureprobe.api.service + +import com.featureprobe.api.entity.OperationLog +import com.featureprobe.api.repository.OperationLogRepository +import spock.lang.Specification + +class OperationLogServiceSpec extends Specification{ + + OperationLogRepository operationLogRepository + + OperationLogService operationLogService + + def setup() { + operationLogRepository = Mock(OperationLogRepository) + operationLogService = new OperationLogService(operationLogRepository) + } + + def "saved operation log"() { + when: + operationLogService.save(new OperationLog("test", "Admin")) + then: + 1 * operationLogRepository.save(_) + } +} + diff --git a/src/test/groovy/com/featureprobe/api/service/UserPasswordAuthenticationSpec.groovy b/src/test/groovy/com/featureprobe/api/service/UserPasswordAuthenticationSpec.groovy index 5a4dd40..8609914 100644 --- a/src/test/groovy/com/featureprobe/api/service/UserPasswordAuthenticationSpec.groovy +++ b/src/test/groovy/com/featureprobe/api/service/UserPasswordAuthenticationSpec.groovy @@ -5,20 +5,27 @@ import com.featureprobe.api.auth.UserPasswordAuthenticationToken import com.featureprobe.api.base.enums.RoleEnum import com.featureprobe.api.entity.Member import com.featureprobe.api.repository.MemberRepository +import com.featureprobe.api.repository.OperationLogRepository import spock.lang.Specification class UserPasswordAuthenticationSpec extends Specification{ def MemberService memberService + OperationLogService operationLogService + + OperationLogRepository operationLogRepository + def UserPasswordAuthenticationProvider userPasswordAuthenticationProvider def UserPasswordAuthenticationToken token def setup() { this.memberService = Mock(MemberService) - this.userPasswordAuthenticationProvider = new UserPasswordAuthenticationProvider(memberService) - token = new UserPasswordAuthenticationToken("admin", "abc12345") + this.operationLogRepository = Mock(OperationLogRepository) + this.operationLogService = new OperationLogService(operationLogRepository) + this.userPasswordAuthenticationProvider = new UserPasswordAuthenticationProvider(memberService, operationLogService) + token = new UserPasswordAuthenticationToken("admin", "", "abc12345") } def "user password is pass"() { @@ -29,6 +36,7 @@ class UserPasswordAuthenticationSpec extends Specification{ password: "\$2a\$10\$jeJ25nROU8APkG2ixK6zyecwzIJ8oHz0ZNqBDiwMXcy9lo9S3YGma", role: RoleEnum.ADMIN)) 1 * memberService.updateVisitedTime("admin") + 1 * operationLogRepository.save(_) with(authenticate) { "Admin" == ((UserPasswordAuthenticationToken)authenticate).getAccount() }