修复用户登录逻辑

This commit is contained in:
yang.xie 2022-10-09 23:26:03 +08:00
parent 75cb0d2445
commit d1dee415b2
14 changed files with 829 additions and 0 deletions

36
pom.xml
View File

@ -40,6 +40,24 @@
<optional>true</optional>
</dependency>
<!-- guava 基础库 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
<dependency>
<groupId>com.configcat</groupId>
<artifactId>configcat-java-client</artifactId>
<version>7.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
@ -87,6 +105,24 @@
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.mailgun</groupId>
<artifactId>mailgun-java</artifactId>
<version>1.0.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--Thymeleaf support email template -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>

View File

@ -0,0 +1,91 @@
/*
* XMLUtils.java
*
* Created on December 6, 2001, 1:21 PM
*/
package com.northtecom.visatrack.api.base.util;
import com.configcat.ConfigCatClient;
import com.configcat.User;
import com.mailgun.api.v3.MailgunMessagesApi;
import com.mailgun.client.MailgunClient;
import com.mailgun.model.message.Message;
import com.mailgun.model.message.MessageResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Utilities for Email sending
*
* @author YuCheng Hu
*/
@Slf4j
@Component
public class EmailUtils {
public static final String EMAIL_KEY_NAME = "emailKey";
public static final String EMAIL_KEY_SENDER_NAME = "emailSender";
public static final String EMAIL_KEY_IDENTIFIER = "MailGun";
public static final String EMAIL_DOMAIN = "mail.usvisatrack.com";
private static String emailSenderAddress = "info@usvisatrack.com";
private final ConfigCatClient configCatClient;
private MailgunMessagesApi mailgunMessagesApi;
@Autowired
public EmailUtils(ConfigCatClient configCatClient) {
this.configCatClient = configCatClient;
User userObject = User.newBuilder().build(EMAIL_KEY_IDENTIFIER);
// Unique identifier is required. Could be UserID, Email address or SessionID.
String emailKey = configCatClient.getValue(String.class, EMAIL_KEY_NAME, userObject, StringUtils.EMPTY);
emailSenderAddress = configCatClient.getValue(String.class, EMAIL_KEY_SENDER_NAME, userObject,
emailSenderAddress);
mailgunMessagesApi = MailgunClient.config(emailKey).createApi(MailgunMessagesApi.class);
}
/**
* Send Test Email to check config and email sending API
*
* @return
*/
public MessageResponse sendTestEmail() {
Message message =
Message.builder().from(emailSenderAddress).to("huyuchengus@gmail.com").subject("TEST").html("<html>\n"
+ "<body>\n" + "\t<h1>Sending HTML emails with Mailgun</h1>\n" + "\t<p style=\"color:blue; " +
"font-size:30px;\">Hello world</p>\n" + "\t<p style=\"font-size:30px;\">More examples can be " +
"found <a href=\"https://www.ossez.com\">here</a></p>\n" + "</body>\n" + "</html>").build();
MessageResponse messageResponse = mailgunMessagesApi.sendMessage(EMAIL_DOMAIN, message);
return messageResponse;
}
public MessageResponse sendTestEmail(String toEmail, String subject) {
Message message = Message.builder().from(emailSenderAddress).to(toEmail).subject(subject).html("<html>\n" +
"<body>\n" + "\t<h1>Sending HTML emails with Mailgun</h1>\n" + "\t<p style=\"color:blue; " +
"font-size:30px;\">Hello world</p>\n" + "\t<p style=\"font-size:30px;\">More examples can be found <a" +
" href=\"https://www.ossez.com\">here</a></p>\n" + "</body>\n" + "</html>").build();
MessageResponse messageResponse = mailgunMessagesApi.sendMessage(EMAIL_DOMAIN, message);
return messageResponse;
}
public MessageResponse sendEmail(String toEmail, String subject, String htmlContent) {
Message message =
Message.builder().from(emailSenderAddress).to(toEmail).subject(subject).html(htmlContent).build();
MessageResponse messageResponse = mailgunMessagesApi.sendMessage(EMAIL_DOMAIN, message);
return messageResponse;
}
}

View File

@ -0,0 +1,109 @@
package com.northtecom.visatrack.api.base.util;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.northtecom.visatrack.api.config.JwtConfig;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
@EnableConfigurationProperties({JwtConfig.class})
@Configuration
public class JwtUtil {
private static final Logger log = LoggerFactory.getLogger(JwtUtil.class);
@Autowired
private JwtConfig jwtConfig;
public JwtUtil() {
}
/**
* @param rememberMe 是否记住登录
* @param useId 用户id
* @param userName 用户名
* @param roles 用户角色
* @param authorities 用户权限
* @return JWT token
*/
public String createJWT(Boolean rememberMe, Long useId, String userName, List<String> roles, Collection<?
extends GrantedAuthority> authorities, Map<String, Object> claims) {
Date now = new Date();
JwtBuilder jwtBuilder = Jwts.builder()
// 将userId放进JWT
.setId(useId.toString())
// 将userName放进JWT
.setSubject(userName)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, this.jwtConfig.getKey())
.claim("roles", roles)
.claim("authorities", authorities);
// 设置过期时间
Long ttl = rememberMe ? this.jwtConfig.getRemember() : this.jwtConfig.getTtl();
if (ttl > 0L) {
jwtBuilder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue()));
}
// 设置附加身份信息
for (String key : claims.keySet()) {
jwtBuilder.claim(key, claims.get(key));
}
String jwt = jwtBuilder.compact();
return jwt;
}
/**
* 解析JWT
*
* @param jwt JWT
* @return {@link Claims}
*/
public Claims parseJWT(String jwt) {
try {
Claims claims = Jwts.parser().setSigningKey(jwtConfig.getKey()).parseClaimsJws(jwt).getBody();
return claims;
} catch (ExpiredJwtException e) {
log.error("Token 已过期");
throw e;
} catch (UnsupportedJwtException e) {
log.error("不支持的 Token");
throw e;
} catch (MalformedJwtException e) {
log.error("Token 无效");
throw e;
} catch (SignatureException e) {
log.error("无效的 Token 签名");
throw e;
} catch (IllegalArgumentException e) {
log.error("Token 参数不存在");
throw e;
}
}
public String getUsernameFromJWT(String jwt) throws ExpiredJwtException, UnsupportedJwtException,
MalformedJwtException, SignatureException, IllegalArgumentException {
Claims claims = this.parseJWT(jwt);
return claims.getSubject();
}
public String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
return StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ") ? bearerToken.substring(7) : null;
}
}

View File

@ -0,0 +1,65 @@
package com.northtecom.visatrack.api.base.web;
import cn.hutool.core.util.StrUtil;
import com.northtecom.visatrack.api.base.util.JwtUtil;
import com.northtecom.visatrack.api.service.impl.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created with IntelliJ IDEA.
*
* @Author: XieYang
* @Date: 2022/10/03/20:29
* @Description:
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String jwt = jwtUtil.getJwtFromRequest(request);
// 没有jwt就放行
if (StrUtil.isBlank(jwt)) {
filterChain.doFilter(request, response);
return;
}
try {
String username = jwtUtil.getUsernameFromJWT(jwt);
UserDetails userDetails = userService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (Exception ex) {
//获取用户信息失败也放行
filterChain.doFilter(request, response);
}
}
}

View File

@ -0,0 +1,21 @@
package com.northtecom.visatrack.api.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* <p>
* 自定义配置
* </p>
*
* @date Created in 2018-12-13 10:56
*/
@Data
@ConfigurationProperties(prefix = "custom.config")
public class CustomConfig {
/**
* 不需要拦截的地址
*/
private IgnoreConfig ignores;
}

View File

@ -0,0 +1,65 @@
package com.northtecom.visatrack.api.config;
import com.google.common.collect.Lists;
import lombok.Data;
import java.util.List;
/**
* <p>
* 忽略配置
* </p>
*
* @author yangkai.shen
* @date Created in 2018-12-17 17:37
*/
@Data
public class IgnoreConfig {
/**
* 需要忽略的 URL 格式不考虑请求方法
*/
private List<String> pattern = Lists.newArrayList();
/**
* 需要忽略的 GET 请求
*/
private List<String> get = Lists.newArrayList();
/**
* 需要忽略的 POST 请求
*/
private List<String> post = Lists.newArrayList();
/**
* 需要忽略的 DELETE 请求
*/
private List<String> delete = Lists.newArrayList();
/**
* 需要忽略的 PUT 请求
*/
private List<String> put = Lists.newArrayList();
/**
* 需要忽略的 HEAD 请求
*/
private List<String> head = Lists.newArrayList();
/**
* 需要忽略的 PATCH 请求
*/
private List<String> patch = Lists.newArrayList();
/**
* 需要忽略的 OPTIONS 请求
*/
private List<String> options = Lists.newArrayList();
/**
* 需要忽略的 TRACE 请求
*/
private List<String> trace = Lists.newArrayList();
}

View File

@ -0,0 +1,28 @@
package com.northtecom.visatrack.api.controller.vo;
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
/**
* Created with IntelliJ IDEA.
*
* @Author: XieYang
* @Date: 2022/10/01/17:08
* @Description:
*/
@Data
public class RegisterUserRequest {
@NotBlank(message = "User name can not be empty.")
private String userName;
@NotBlank(message = "Password can not be empty.")
private String password;
@NotBlank(message = "Confirm password can not be empty.")
private String confirmPassword;
@Email(message = "Email format is not correct.")
@NotBlank(message = "Email can not be empty.")
private String email;
private String nationality;
private Boolean allowReceiveMessage;
}

View File

@ -0,0 +1,71 @@
package com.northtecom.visatrack.api.controller.vo;
import com.northtecom.visatrack.api.data.entity.User;
import com.northtecom.visatrack.api.service.enums.UserStatus;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
/**
* Created with IntelliJ IDEA.
*
* @Author: XieYang
* @Date: 2022/10/03/20:02
* @Description:
*/
@Data
public class VisaTrackUserDetail implements UserDetails {
private User user;
public VisaTrackUserDetail(User user) {
this.user = user;
}
public Long getId() {
return user.getId();
}
public String getSurname() {
return user.getUserName();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
@Override
public String getPassword() {
return this.user.getUserPassword();
}
@Override
public String getUsername() {
return this.user.getUserEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.user.getUserStatus() == UserStatus.ACTIVATED && this.user.getIsEmailVerified();
}
}

View File

@ -0,0 +1,12 @@
package com.northtecom.visatrack.api.data.repository;
import com.northtecom.visatrack.api.data.entity.Blog;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
/**
*
*/
@Repository
public interface BlogRepository extends PagingAndSortingRepository<Blog, Long> {
}

View File

@ -0,0 +1,23 @@
package com.northtecom.visatrack.api.data.repository;
import com.northtecom.visatrack.api.data.entity.User;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
import java.util.Optional;
/**
*
*/
public interface UserRepository extends CrudRepository<User, Long> {
User findByUserNameEquals(String userName);
Optional<User> findByUserEmail(String userEmail);
Optional<User> findByUserName(String userName);
User findByUserNameEqualsAndUserPasswordEquals(String userName, String userPassword);
List<User> findByUserNameContaining(String username);
}

View File

@ -0,0 +1,38 @@
package com.northtecom.visatrack.api.service.impl;
import com.northtecom.visatrack.api.data.entity.Blog;
import com.northtecom.visatrack.api.data.repository.BlogRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
/**
* Listing Service.
* This Service process listing related.
*
* @author YuCheng Hu
*/
@Service
@Slf4j
public class ContentService {
private final BlogRepository blogRepository;
@Autowired
public ContentService(BlogRepository blogRepository) {
this.blogRepository = blogRepository;
}
public Page<Blog> getNewBlog(Pageable pageable) {
return blogRepository.findAll(pageable);
}
public Blog getBlogById(Long blogId) {
return blogRepository.findById(blogId).orElse(null);
}
}

View File

@ -0,0 +1,66 @@
package com.northtecom.visatrack.api.service.impl;
import com.northtecom.visatrack.api.base.util.EmailUtils;
import com.northtecom.visatrack.api.data.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
/**
* Created with IntelliJ IDEA.
*
* @Author: XieYang
* @Date: 2022/10/02/10:37
* @Description:
*/
@Service
@Slf4j
public class EmailService {
private final TemplateService templateService;
private final EmailUtils emailUtils;
@Autowired
public EmailService(TemplateService templateService, EmailUtils emailUtils) {
this.templateService = templateService;
this.emailUtils = emailUtils;
}
public void sendVerifiedEmailToUser(User user) {
log.info("Send verified email to user: {}", user);
HashMap<String, Object> map = new HashMap<>();
map.put("SiteName", "UsVisaTrack");
map.put("SiteHost", "www.usvisatrack.com");
map.put("UserName", user.getUserName());
map.put("ActiveUserId", user.getId());
map.put("ActiveCode", user.getEmailVerifiedCode());
String htmlContent = this.templateService.createHtml("email/VerifiedEmail.html", map);
String subject = "UsVisaTrack Email Verification";
emailUtils.sendEmail(user.getUserEmail(), subject, htmlContent);
}
public void sendWelcomeEmailToUser(User user) {
log.info("Send welcome email to user: {}", user);
HashMap<String, Object> map = new HashMap<>();
map.put("SiteName", "UsVisaTrack");
map.put("SiteHost", "www.usvisatrack.com");
map.put("UserName", user.getUserName());
map.put("ActiveUserId", user.getId());
String htmlContent = this.templateService.createHtml("email/Welcome.html", map);
String subject = "Welcome to UsVisaTrack";
emailUtils.sendEmail(user.getUserEmail(), subject, htmlContent);
}
}

View File

@ -0,0 +1,35 @@
package com.northtecom.visatrack.api.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.util.Map;
/**
* Created with IntelliJ IDEA.
*
* @Author: XieYang
* @Date: 2022/10/01/16:53
* @Description:
*/
@Slf4j
@Component
public class TemplateService {
@Autowired
TemplateEngine templateEngine;
public String createHtml(String templateName, Map<String, Object> map) {
Context context = new Context();
if (map.size() > 0) {
context.setVariables(map);
}
String templateContent = templateEngine.process(templateName, context);
log.info("template Content: {}", templateContent);
return templateContent;
}
}

View File

@ -0,0 +1,169 @@
package com.northtecom.visatrack.api.service.impl;
import cn.hutool.core.util.RandomUtil;
import com.northtecom.visatrack.api.base.exception.BaseException;
import com.northtecom.visatrack.api.base.util.JwtUtil;
import com.northtecom.visatrack.api.base.web.Status;
import com.northtecom.visatrack.api.controller.vo.RegisterUserRequest;
import com.northtecom.visatrack.api.controller.vo.VisaTrackUserDetail;
import com.northtecom.visatrack.api.data.entity.User;
import com.northtecom.visatrack.api.data.repository.UserRepository;
import com.northtecom.visatrack.api.service.enums.UserStatus;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Listing Service.
* This Service process listing related.
*
* @author YuCheng Hu
*/
@Service
@Slf4j
public class UserService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
@Autowired
private JwtUtil jwtUtil;
/**
* Search Database by userName.
* If not in DB, insert to Register user.
*
* @param user
* @return
*/
public User userRegister(User user) {
if (ObjectUtils.isNotEmpty(user)) {
user.setUserStatus(UserStatus.NEEDVERIFY);
user.setDateRegistered(LocalDateTime.now());
return userRepository.save(user);
}
return null;
}
public User userLogin(String userName, String userPassword) {
return userRepository.findByUserNameEqualsAndUserPasswordEquals(userName, userPassword);
}
public User findUserByUserName(String userName) {
return userRepository.findByUserNameEquals(userName);
}
public User registerUser(RegisterUserRequest registerUserRequest) {
if (checkUserNameIsExist(registerUserRequest.getUserName())) {
throw new BaseException(Status.BAD_REQUEST, "User Name is already exist");
}
if (checkEmailIsExist(registerUserRequest.getEmail())) {
throw new BaseException(Status.BAD_REQUEST, "Email is already exist");
}
User user = new User();
user.setUserName(registerUserRequest.getUserName());
user.setUserPassword(registerUserRequest.getPassword());
user.setUserEmail(registerUserRequest.getEmail());
user.setUserStatus(UserStatus.NEEDVERIFY);
user.setDateRegistered(LocalDateTime.now());
user.setIsEmailVerified(false);
user.setEmailVerifiedCode(RandomUtil.randomString(18));
User savedUser = userRepository.save(user);
emailService.sendVerifiedEmailToUser(savedUser);
return savedUser;
}
private Boolean checkEmailIsExist(String email) {
return userRepository.findByUserEmail(email).isPresent();
}
private Boolean checkUserNameIsExist(String userName) {
return userRepository.findByUserName(userName).isPresent();
}
public void activeUser(Long userId, String verifiedCode) {
User user = userRepository.findById(userId).orElseThrow(() -> new BaseException(Status.BAD_REQUEST, "User is " +
"not exist"));
if (user.getIsEmailVerified()) {
throw new BaseException(Status.BAD_REQUEST, "User is already verified");
}
if (!StringUtils.hasText(verifiedCode) || !verifiedCode.equals(user.getEmailVerifiedCode())) {
throw new BaseException(Status.BAD_REQUEST, "Verified Code is not correct");
}
user.setIsEmailVerified(true);
user.setEmailVerifiedCode("");
user.setUserStatus(UserStatus.ACTIVATED);
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmailToUser(savedUser);
}
public boolean checkPasswordIsMatch(String userPassword, String password) {
return userPassword.equals(password);
}
private String generateToken(User user, Boolean rememberMe) {
VisaTrackUserDetail visaTrackUserDetail = new VisaTrackUserDetail(user);
return this.generateToken(visaTrackUserDetail, rememberMe);
}
public String generateToken(VisaTrackUserDetail userDetail, Boolean rememberMe) {
Map<String, Object> userClaims = new HashMap<String, Object>();
userClaims.put("user_id", userDetail.getId());
userClaims.put("login_id", userDetail.getUsername());
userClaims.put("surname", userDetail.getSurname());
userClaims.put("email", userDetail.getUsername());
return this.jwtUtil.createJWT(rememberMe, userDetail.getId(), userDetail.getUsername(), Arrays.asList(),
Arrays.asList(), userClaims);
}
private String generateToken(Authentication authentication, Boolean rememberMe) {
VisaTrackUserDetail userDetail = (VisaTrackUserDetail) authentication.getPrincipal();
return this.generateToken(userDetail.getUser(), rememberMe);
}
public User findUserByEmail(String userEmail) {
return userRepository.findByUserEmail(userEmail).orElse(null);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUserEmail(username).orElseThrow(() -> new UsernameNotFoundException("User " +
"not found"));
if (user.getUserStatus() == UserStatus.NEEDVERIFY) {
throw new BaseException(Status.BAD_REQUEST, "User is not verified");
}
if (!user.getIsEmailVerified()) {
throw new BaseException(Status.BAD_REQUEST, "User email is not verified");
}
return new VisaTrackUserDetail(user);
}
}