diff --git a/pom.xml b/pom.xml index 47135c32..62ec8b8a 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,11 @@ org.springframework.boot spring-boot-starter-aop + + com.github.whvcse + easy-captcha + 1.6.2 + org.springdoc springdoc-openapi-starter-webmvc-ui diff --git a/src/main/java/cn/lihongjie/coal/common/Ctx.java b/src/main/java/cn/lihongjie/coal/common/Ctx.java new file mode 100644 index 00000000..69bba7c3 --- /dev/null +++ b/src/main/java/cn/lihongjie/coal/common/Ctx.java @@ -0,0 +1,28 @@ +package cn.lihongjie.coal.common; + + +import cn.lihongjie.coal.service.SessionService; +import lombok.experimental.UtilityClass; +import org.springframework.security.core.context.SecurityContextHolder; + +@UtilityClass +public class Ctx { + + + + public static String getUserId(){ + return getAuthentication().getUser().getId(); + + } + + + public static String getSessionId(){ + return getAuthentication().getSessionId(); + + } + + + private static SessionService.MyAuthentication getAuthentication() { + return (SessionService.MyAuthentication) SecurityContextHolder.getContext().getAuthentication(); + } +} diff --git a/src/main/java/cn/lihongjie/coal/config/RedisConfig.java b/src/main/java/cn/lihongjie/coal/config/RedisConfig.java new file mode 100644 index 00000000..c8e88a1a --- /dev/null +++ b/src/main/java/cn/lihongjie/coal/config/RedisConfig.java @@ -0,0 +1,18 @@ +package cn.lihongjie.coal.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; + +@Configuration +@Slf4j +public class RedisConfig { + + @Autowired + RedisConnectionFactory redisConnectionFactory; + + +} diff --git a/src/main/java/cn/lihongjie/coal/controller/LoginController.java b/src/main/java/cn/lihongjie/coal/controller/LoginController.java new file mode 100644 index 00000000..12e31b9a --- /dev/null +++ b/src/main/java/cn/lihongjie/coal/controller/LoginController.java @@ -0,0 +1,56 @@ +package cn.lihongjie.coal.controller; + +import cn.lihongjie.coal.annotation.SysLog; +import cn.lihongjie.coal.common.Ctx; +import cn.lihongjie.coal.dto.CaptchaDto; +import cn.lihongjie.coal.dto.LoginDto; +import cn.lihongjie.coal.dto.UserDto; +import cn.lihongjie.coal.service.SessionService; +import cn.lihongjie.coal.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping +public class LoginController { + + @Autowired + SessionService service; + + @Autowired + UserService userService; + + @PostMapping("/login") + @SysLog(msg = "登录") + public UserDto login(@RequestBody LoginDto dto) { + this.service.login(dto); + + return userService.getById(Ctx.getUserId()); + + } + + @PostMapping("/genCaptcha") + public CaptchaDto genCaptcha() { + return this.service.genCaptcha(); + + + } + + @PostMapping("/logout") + @SysLog(msg = "退出") + public Object logout() { + this.service.logout(); + return null; + } + + @PostMapping("/isValid") + public Boolean isValid() { + + return Ctx.getUserId() != null; + } + + +} diff --git a/src/main/java/cn/lihongjie/coal/dao/UserSessionRepository.java b/src/main/java/cn/lihongjie/coal/dao/UserSessionRepository.java deleted file mode 100644 index 9b6141c2..00000000 --- a/src/main/java/cn/lihongjie/coal/dao/UserSessionRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package cn.lihongjie.coal.dao; - -import cn.lihongjie.coal.entity.UserSessionEntity; -import org.springframework.stereotype.Repository; - -@Repository -public interface UserSessionRepository extends BaseRepository { -} \ No newline at end of file diff --git a/src/main/java/cn/lihongjie/coal/dto/CaptchaDto.java b/src/main/java/cn/lihongjie/coal/dto/CaptchaDto.java new file mode 100644 index 00000000..037dcbc0 --- /dev/null +++ b/src/main/java/cn/lihongjie/coal/dto/CaptchaDto.java @@ -0,0 +1,16 @@ +package cn.lihongjie.coal.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CaptchaDto { + + private String id; + + private String base64; +} diff --git a/src/main/java/cn/lihongjie/coal/dto/LoginDto.java b/src/main/java/cn/lihongjie/coal/dto/LoginDto.java new file mode 100644 index 00000000..07aa1f70 --- /dev/null +++ b/src/main/java/cn/lihongjie/coal/dto/LoginDto.java @@ -0,0 +1,13 @@ +package cn.lihongjie.coal.dto; + + +import lombok.Data; + +@Data +public class LoginDto { + + private String username; + private String password; + private String captchaId; + private String captcha; +} diff --git a/src/main/java/cn/lihongjie/coal/entity/UserSessionEntity.java b/src/main/java/cn/lihongjie/coal/entity/UserSessionEntity.java deleted file mode 100644 index fcbf379b..00000000 --- a/src/main/java/cn/lihongjie/coal/entity/UserSessionEntity.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.lihongjie.coal.entity; - -import cn.lihongjie.coal.entity.base.OrgBaseEntity; -import jakarta.persistence.*; -import lombok.Data; -import lombok.Getter; -import lombok.Setter; - -import java.time.LocalDateTime; - -@Data -@Entity -@Getter -@Setter -public class UserSessionEntity extends OrgBaseEntity { - - - - @ManyToOne - @JoinColumn(name = "user_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) - private UserEntity user; - - - private LocalDateTime expireAt; - - -} diff --git a/src/main/java/cn/lihongjie/coal/entity/base/CommonEntity.java b/src/main/java/cn/lihongjie/coal/entity/base/CommonEntity.java index e63b6425..a6a8d132 100644 --- a/src/main/java/cn/lihongjie/coal/entity/base/CommonEntity.java +++ b/src/main/java/cn/lihongjie/coal/entity/base/CommonEntity.java @@ -1,11 +1,12 @@ package cn.lihongjie.coal.entity.base; -import cn.lihongjie.coal.entity.base.BaseEntity; import jakarta.persistence.MappedSuperclass; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.Comment; +import java.util.Objects; + @MappedSuperclass @Getter @Setter @@ -30,4 +31,10 @@ public class CommonEntity extends BaseEntity { @Comment("常用状态 0 禁用 1 启用") private Integer status; + + + public boolean isDisabled(){ + return Objects.equals(status, 0); + + } } diff --git a/src/main/java/cn/lihongjie/coal/service/SessionService.java b/src/main/java/cn/lihongjie/coal/service/SessionService.java new file mode 100644 index 00000000..9eaa4abe --- /dev/null +++ b/src/main/java/cn/lihongjie/coal/service/SessionService.java @@ -0,0 +1,180 @@ +package cn.lihongjie.coal.service; + + +import cn.lihongjie.coal.dto.CaptchaDto; +import cn.lihongjie.coal.dto.LoginDto; +import cn.lihongjie.coal.entity.UserEntity; +import cn.lihongjie.coal.exception.BizException; +import com.wf.captcha.SpecCaptcha; +import com.wf.captcha.base.Captcha; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +public class SessionService { + + @Autowired + UserService userService; + + @Autowired + StringRedisTemplate stringRedisTemplate; + + + /** + * 生成验证码 + * + * @return + */ + public CaptchaDto genCaptcha() { + // 三个参数分别为宽、高、位数 + SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4); + // 设置字体 + // 设置类型,纯数字、纯字母、字母数字混合 + specCaptcha.setCharType(Captcha.TYPE_ONLY_NUMBER); + + String text = specCaptcha.text(); + + String id = UUID.randomUUID().toString(); + + + String base64 = specCaptcha.toBase64(); + + stringRedisTemplate.opsForValue().set(id, text, 5, TimeUnit.MINUTES); + + return new CaptchaDto(id, base64); + + + } + + public void login(LoginDto dto) { + + + String captchaId = dto.getCaptchaId(); + + String expectCaptcha = stringRedisTemplate.opsForValue().get(captchaId); + + if (expectCaptcha == null) { + throw new BizException("验证码已失效"); + } + + if (!StringUtils.equals(expectCaptcha, dto.getCaptcha())) { + throw new BizException("验证码错误"); + } + + + UserEntity user = userService.findByUsername(dto.getUsername()).orElseThrow(() -> new BizException("用户名或者密码错误")); + + + if (user.isDisabled()) { + throw new BizException("用户被禁用"); + } + + if (user.getOrganization().isDisabled()) { + throw new BizException("用户所在单位被禁用"); + } + + if (!userService.isValidPassword(dto.getPassword(), user.getPassword())) { + throw new BizException("用户名或者密码错误"); + } + + String sessionId = UUID.randomUUID().toString(); + + + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + context.setAuthentication(new MyAuthentication(dto, user, sessionId)); + + stringRedisTemplate.opsForValue().set(sessionId, user.getId(), 1, TimeUnit.HOURS); + + } + + + public void buildSession(String sessionId){ + + String userId = stringRedisTemplate.opsForValue().getAndExpire(sessionId, 1, TimeUnit.HOURS); + + if (StringUtils.isEmpty(userId)) { + throw new BizException("会话已过期,请重新登录"); + } + + UserEntity user = userService.get(userId); + + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + context.setAuthentication(new MyAuthentication(null, user, sessionId)); + + + } + + + + public void logout() { + + String sessionId = ((MyAuthentication) SecurityContextHolder.getContext().getAuthentication()).sessionId; + stringRedisTemplate.opsForValue().getAndDelete(sessionId); + + } + + + @Data + public static class MyAuthentication implements Authentication { + private final LoginDto dto; + private final UserEntity user; + private final String sessionId; + + public MyAuthentication(LoginDto dto, UserEntity user, String sessionId) { + this.dto = dto; + this.user = user; + this.sessionId = sessionId; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public Object getCredentials() { + return dto; + } + + @Override + public Object getDetails() { + return user; + } + + @Override + public Object getPrincipal() { + return user; + } + + @Override + public boolean isAuthenticated() { + return true; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + + } + + @Override + public String getName() { + return user.getUsername(); + } + + } +} diff --git a/src/main/java/cn/lihongjie/coal/service/UserService.java b/src/main/java/cn/lihongjie/coal/service/UserService.java index 836c4743..22b5574d 100644 --- a/src/main/java/cn/lihongjie/coal/service/UserService.java +++ b/src/main/java/cn/lihongjie/coal/service/UserService.java @@ -6,6 +6,10 @@ import cn.lihongjie.coal.entity.UserEntity; import cn.lihongjie.coal.exception.BizException; import cn.lihongjie.coal.mapper.UserMapper; import jakarta.annotation.PostConstruct; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -13,9 +17,12 @@ import org.springframework.core.convert.ConversionService; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.Optional; + @Service @Slf4j public class UserService extends BaseService{ @@ -112,4 +119,21 @@ public class UserService extends BaseService{ return page.map(this.mapper::toDto); } + + public Optional findByUsername(String username) { + + + return this.search(new Specification() { + @Override + public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) { + return criteriaBuilder.equal(root.get("username"), username); + } + }).stream().findFirst(); + + + } + + public boolean isValidPassword(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } } diff --git a/src/main/java/cn/lihongjie/coal/service/UserSessionService.java b/src/main/java/cn/lihongjie/coal/service/UserSessionService.java deleted file mode 100644 index 9226e5c1..00000000 --- a/src/main/java/cn/lihongjie/coal/service/UserSessionService.java +++ /dev/null @@ -1,91 +0,0 @@ -package cn.lihongjie.coal.service; - -import cn.lihongjie.coal.dao.UserSessionRepository; -import cn.lihongjie.coal.entity.UserEntity; -import cn.lihongjie.coal.entity.UserSessionEntity; -import jakarta.annotation.PostConstruct; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections4.CollectionUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -@Service -@Slf4j -public class UserSessionService { - - @Autowired - UserSessionRepository repository; - - - @PostConstruct - public void init(){ - - Executors.newScheduledThreadPool(1).schedule(this::autoExpire, 1, TimeUnit.MINUTES); - - - } - - private void autoExpire() { - - List expireAt = this.repository.findAll(new Specification() { - @Override - public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) { - return criteriaBuilder.lessThanOrEqualTo(root.get("expireAt"), LocalDateTime.now()); - } - - }); - - if (CollectionUtils.isNotEmpty(expireAt)) { - log.info("清理获取会话 {} 条", expireAt.size()); - for (UserSessionEntity session : expireAt) { - logout(session.getId()); - - } - } - - - } - - - public UserSessionEntity login(UserEntity user) { - - UserSessionEntity session = new UserSessionEntity(); - - session.setUser(user); - - session.setExpireAt(LocalDateTime.now().plusHours(1)); - - repository.save(session); - - return session; - - } - - public void refresh(UserSessionEntity session) { - - session.setExpireAt(LocalDateTime.now().plusHours(1)); - repository.save(session); - } - - - public void logout(String sessionId) { - - repository.deleteById(sessionId); - } - - - public Optional getById(String sessionId) { - return repository.findById(sessionId); - } -}