登录接口

This commit is contained in:
2023-07-30 22:39:06 +08:00
parent 63f0553db9
commit 369ff72754
12 changed files with 348 additions and 127 deletions

View File

@@ -50,6 +50,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>

View File

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

View File

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

View File

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

View File

@@ -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<UserSessionEntity> {
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<? extends GrantedAuthority> 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();
}
}
}

View File

@@ -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<UserEntity, UserRepository>{
@@ -112,4 +119,21 @@ public class UserService extends BaseService<UserEntity, UserRepository>{
return page.map(this.mapper::toDto);
}
public Optional<UserEntity> findByUsername(String username) {
return this.search(new Specification<UserEntity>() {
@Override
public Predicate toPredicate(Root<UserEntity> 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);
}
}

View File

@@ -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<UserSessionEntity> expireAt = this.repository.findAll(new Specification<UserSessionEntity>() {
@Override
public Predicate toPredicate(Root<UserSessionEntity> 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<UserSessionEntity> getById(String sessionId) {
return repository.findById(sessionId);
}
}