mirror of https://github.com/halo-dev/halo
Complete user login
parent
90b7007b6e
commit
b547e0720b
|
@ -2,6 +2,9 @@ package cc.ryanc.halo.repository;
|
||||||
|
|
||||||
import cc.ryanc.halo.model.entity.User;
|
import cc.ryanc.halo.model.entity.User;
|
||||||
import cc.ryanc.halo.repository.base.BaseRepository;
|
import cc.ryanc.halo.repository.base.BaseRepository;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User repository.
|
* User repository.
|
||||||
|
@ -10,4 +13,12 @@ import cc.ryanc.halo.repository.base.BaseRepository;
|
||||||
*/
|
*/
|
||||||
public interface UserRepository extends BaseRepository<User, Integer> {
|
public interface UserRepository extends BaseRepository<User, Integer> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets user by username.
|
||||||
|
*
|
||||||
|
* @param username username must not be blank
|
||||||
|
* @return an optional user
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
Optional<User> findByUsername(@NonNull String username);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,11 @@ public class UserDetail {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets user info.
|
||||||
|
*
|
||||||
|
* @param user user info
|
||||||
|
*/
|
||||||
public void setUser(User user) {
|
public void setUser(User user) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package cc.ryanc.halo.service;
|
package cc.ryanc.halo.service;
|
||||||
|
|
||||||
|
import cc.ryanc.halo.exception.NotFoundException;
|
||||||
import cc.ryanc.halo.model.entity.User;
|
import cc.ryanc.halo.model.entity.User;
|
||||||
import cc.ryanc.halo.service.base.CrudService;
|
import cc.ryanc.halo.service.base.CrudService;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User service.
|
* User service.
|
||||||
|
@ -10,4 +14,43 @@ import cc.ryanc.halo.service.base.CrudService;
|
||||||
*/
|
*/
|
||||||
public interface UserService extends CrudService<User, Integer> {
|
public interface UserService extends CrudService<User, Integer> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login failure count key.
|
||||||
|
*/
|
||||||
|
String LOGIN_FAILURE_COUNT_KEY = "login.failure.count";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max login try count.
|
||||||
|
*/
|
||||||
|
int MAX_LOGIN_TRY = 5;
|
||||||
|
|
||||||
|
int LOCK_MINUTES = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets user by username.
|
||||||
|
*
|
||||||
|
* @param username username must not be blank
|
||||||
|
* @return an optional user
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
Optional<User> getByUsername(@NonNull String username);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets non null user by username.
|
||||||
|
*
|
||||||
|
* @return user info
|
||||||
|
* @throws NotFoundException throws when the username does not exist
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
User getByUsernameOfNonNull(@NonNull String username);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logins by username and password.
|
||||||
|
*
|
||||||
|
* @param username username must not be blank
|
||||||
|
* @param password password must not be blank
|
||||||
|
* @return user info
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
User login(@NonNull String username, @NonNull String password);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,19 @@
|
||||||
package cc.ryanc.halo.service.impl;
|
package cc.ryanc.halo.service.impl;
|
||||||
|
|
||||||
|
import cc.ryanc.halo.cache.StringCacheStore;
|
||||||
|
import cc.ryanc.halo.exception.BadRequestException;
|
||||||
|
import cc.ryanc.halo.exception.NotFoundException;
|
||||||
import cc.ryanc.halo.model.entity.User;
|
import cc.ryanc.halo.model.entity.User;
|
||||||
import cc.ryanc.halo.repository.UserRepository;
|
import cc.ryanc.halo.repository.UserRepository;
|
||||||
import cc.ryanc.halo.service.UserService;
|
import cc.ryanc.halo.service.UserService;
|
||||||
import cc.ryanc.halo.service.base.AbstractCrudService;
|
import cc.ryanc.halo.service.base.AbstractCrudService;
|
||||||
|
import cc.ryanc.halo.utils.DateUtils;
|
||||||
|
import cn.hutool.crypto.digest.BCrypt;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserService implementation class
|
* UserService implementation class
|
||||||
|
@ -17,8 +26,64 @@ public class UserServiceImpl extends AbstractCrudService<User, Integer> implemen
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
public UserServiceImpl(UserRepository userRepository) {
|
private final StringCacheStore stringCacheStore;
|
||||||
|
|
||||||
|
public UserServiceImpl(UserRepository userRepository,
|
||||||
|
StringCacheStore stringCacheStore) {
|
||||||
super(userRepository);
|
super(userRepository);
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.stringCacheStore = stringCacheStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<User> getByUsername(String username) {
|
||||||
|
return userRepository.findByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getByUsernameOfNonNull(String username) {
|
||||||
|
return getByUsername(username).orElseThrow(() -> new NotFoundException("The username dose not exist").setErrorData(username));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User login(String username, String password) {
|
||||||
|
Assert.hasText(username, "Username must not be blank");
|
||||||
|
Assert.hasText(password, "Password must not be blank");
|
||||||
|
|
||||||
|
// Ger user by username
|
||||||
|
User user = getByUsernameOfNonNull(username);
|
||||||
|
|
||||||
|
// Check expiration
|
||||||
|
if (user.getExpireTime() != null && DateUtils.now().before(user.getExpireTime())) {
|
||||||
|
// If expired
|
||||||
|
// TODO replace by i18n
|
||||||
|
throw new BadRequestException("You have been locked temporarily");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!BCrypt.checkpw(password, user.getPassword())) {
|
||||||
|
// If the password is mismatched
|
||||||
|
// Add login failure count
|
||||||
|
Integer loginFailureCount = stringCacheStore.get(LOGIN_FAILURE_COUNT_KEY).map(countString -> Integer.valueOf(countString)).orElse(0);
|
||||||
|
|
||||||
|
if (loginFailureCount >= MAX_LOGIN_TRY) {
|
||||||
|
// Set expiration
|
||||||
|
user.setExpireTime(org.apache.commons.lang3.time.DateUtils.addMilliseconds(DateUtils.now(), LOCK_MINUTES));
|
||||||
|
// Update user
|
||||||
|
update(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
loginFailureCount++;
|
||||||
|
|
||||||
|
stringCacheStore.put(LOGIN_FAILURE_COUNT_KEY, loginFailureCount.toString(), LOCK_MINUTES, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
// TODO replace by i18n
|
||||||
|
throw new BadRequestException("Username or password is mismatched, last " + (MAX_LOGIN_TRY - loginFailureCount) + " retry(s)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Set session
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package cc.ryanc.halo.utils;
|
||||||
|
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date utilities.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @date 3/18/19
|
||||||
|
*/
|
||||||
|
public class DateUtils {
|
||||||
|
|
||||||
|
private DateUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets current date.
|
||||||
|
*
|
||||||
|
* @return current date
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static Date now() {
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
package cc.ryanc.halo.utils;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json utilities.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @date 3/18/19
|
||||||
|
*/
|
||||||
|
public class JsonUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default json mapper.
|
||||||
|
*/
|
||||||
|
public final static ObjectMapper DEFAULT_JSON_MAPPER = createDefaultJsonMapper();
|
||||||
|
|
||||||
|
private JsonUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a default json mapper.
|
||||||
|
*
|
||||||
|
* @return object mapper
|
||||||
|
*/
|
||||||
|
public static ObjectMapper createDefaultJsonMapper() {
|
||||||
|
return createDefaultJsonMapper(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a default json mapper.
|
||||||
|
*
|
||||||
|
* @param strategy property naming strategy
|
||||||
|
* @return object mapper
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static ObjectMapper createDefaultJsonMapper(@Nullable PropertyNamingStrategy strategy) {
|
||||||
|
// Create object mapper
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
// Configure
|
||||||
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
// Set property naming strategy
|
||||||
|
if (strategy != null) {
|
||||||
|
mapper.setPropertyNamingStrategy(strategy);
|
||||||
|
}
|
||||||
|
return mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts json to the object specified type.
|
||||||
|
*
|
||||||
|
* @param json json content must not be blank
|
||||||
|
* @param type object type must not be null
|
||||||
|
* @param <T> target object type
|
||||||
|
* @return object specified type
|
||||||
|
* @throws IOException throws when fail to convert
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static <T> T jsonToObject(@NonNull String json, @NonNull Class<T> type) throws IOException {
|
||||||
|
return jsonToObject(json, type, DEFAULT_JSON_MAPPER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts json to the object specified type.
|
||||||
|
*
|
||||||
|
* @param json json content must not be blank
|
||||||
|
* @param type object type must not be null
|
||||||
|
* @param objectMapper object mapper must not be null
|
||||||
|
* @param <T> target object type
|
||||||
|
* @return object specified type
|
||||||
|
* @throws IOException throws when fail to convert
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static <T> T jsonToObject(@NonNull String json, @NonNull Class<T> type, @NonNull ObjectMapper objectMapper) throws IOException {
|
||||||
|
Assert.hasText(json, "Json content must not be blank");
|
||||||
|
Assert.notNull(type, "Target type must not be null");
|
||||||
|
Assert.notNull(objectMapper, "Object mapper must not null");
|
||||||
|
|
||||||
|
return objectMapper.readValue(json, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Converts input stream to object specified type.
|
||||||
|
// *
|
||||||
|
// * @param inputStream input stream must not be null
|
||||||
|
// * @param type object type must not be null
|
||||||
|
// * @param <T> target object type
|
||||||
|
// * @return object specified type
|
||||||
|
// * @throws IOException throws when fail to convert
|
||||||
|
// */
|
||||||
|
// @NonNull
|
||||||
|
// public static <T> T inputStreamToObject(@NonNull InputStream inputStream, @NonNull Class<T> type) throws IOException {
|
||||||
|
// return inputStreamToObject(inputStream, type, null);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Converts input stream to object specified type.
|
||||||
|
// *
|
||||||
|
// * @param inputStream input stream must not be null
|
||||||
|
// * @param type object type must not be null
|
||||||
|
// * @param objectMapper object mapper must not be null
|
||||||
|
// * @param <T> target object type
|
||||||
|
// * @return object specified type
|
||||||
|
// * @throws IOException throws when fail to convert
|
||||||
|
// */
|
||||||
|
// @NonNull
|
||||||
|
// public static <T> T inputStreamToObject(@NonNull InputStream inputStream, @NonNull Class<T> type, @NonNull ObjectMapper objectMapper) throws IOException {
|
||||||
|
// Assert.notNull(inputStream, "Input stream must not be null");
|
||||||
|
//
|
||||||
|
// String json = IOUtils.toString(inputStream);
|
||||||
|
// return jsonToObject(json, type, objectMapper);
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts object to json format.
|
||||||
|
*
|
||||||
|
* @param source source object must not be null
|
||||||
|
* @return json format of the source object
|
||||||
|
* @throws JsonProcessingException throws when fail to convert
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static String objectToJson(@NonNull Object source) throws JsonProcessingException {
|
||||||
|
return objectToJson(source, DEFAULT_JSON_MAPPER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts object to json format.
|
||||||
|
*
|
||||||
|
* @param source source object must not be null
|
||||||
|
* @param objectMapper object mapper must not be null
|
||||||
|
* @return json format of the source object
|
||||||
|
* @throws JsonProcessingException throws when fail to convert
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static String objectToJson(@NonNull Object source, @NonNull ObjectMapper objectMapper) throws JsonProcessingException {
|
||||||
|
Assert.notNull(source, "Source object must not be null");
|
||||||
|
Assert.notNull(objectMapper, "Object mapper must not null");
|
||||||
|
|
||||||
|
return objectMapper.writeValueAsString(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a map to the object specified type.
|
||||||
|
*
|
||||||
|
* @param sourceMap source map must not be empty
|
||||||
|
* @param type object type must not be null
|
||||||
|
* @param <T> target object type
|
||||||
|
* @return the object specified type
|
||||||
|
* @throws IOException throws when fail to convert
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static <T> T mapToObject(@NonNull Map<String, ?> sourceMap, @NonNull Class<T> type) throws IOException {
|
||||||
|
return mapToObject(sourceMap, type, DEFAULT_JSON_MAPPER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a map to the object specified type.
|
||||||
|
*
|
||||||
|
* @param sourceMap source map must not be empty
|
||||||
|
* @param type object type must not be null
|
||||||
|
* @param objectMapper object mapper must not be null
|
||||||
|
* @param <T> target object type
|
||||||
|
* @return the object specified type
|
||||||
|
* @throws IOException throws when fail to convert
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static <T> T mapToObject(@NonNull Map<String, ?> sourceMap, @NonNull Class<T> type, @NonNull ObjectMapper objectMapper) throws IOException {
|
||||||
|
Assert.notEmpty(sourceMap, "Source map must not be empty");
|
||||||
|
|
||||||
|
// Serialize the map
|
||||||
|
String json = objectToJson(sourceMap, objectMapper);
|
||||||
|
|
||||||
|
// Deserialize the json format of the map
|
||||||
|
return jsonToObject(json, type, objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a source object to a map
|
||||||
|
*
|
||||||
|
* @param source source object must not be null
|
||||||
|
* @return a map
|
||||||
|
* @throws IOException throws when fail to convert
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static Map<?, ?> objectToMap(@NonNull Object source) throws IOException {
|
||||||
|
return objectToMap(source, DEFAULT_JSON_MAPPER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a source object to a map
|
||||||
|
*
|
||||||
|
* @param source source object must not be null
|
||||||
|
* @param objectMapper object mapper must not be null
|
||||||
|
* @return a map
|
||||||
|
* @throws IOException throws when fail to convert
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static Map<?, ?> objectToMap(@NonNull Object source, @NonNull ObjectMapper objectMapper) throws IOException {
|
||||||
|
|
||||||
|
// Serialize the source object
|
||||||
|
String json = objectToJson(source, objectMapper);
|
||||||
|
|
||||||
|
// Deserialize the json
|
||||||
|
return jsonToObject(json, Map.class, objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,17 +1,15 @@
|
||||||
package cc.ryanc.halo.web.controller.admin;
|
package cc.ryanc.halo.web.controller.admin;
|
||||||
|
|
||||||
import cc.ryanc.halo.model.dto.LogOutputDTO;
|
import cc.ryanc.halo.model.dto.LogOutputDTO;
|
||||||
|
import cc.ryanc.halo.model.dto.UserOutputDTO;
|
||||||
import cc.ryanc.halo.model.dto.post.PostSimpleOutputDTO;
|
import cc.ryanc.halo.model.dto.post.PostSimpleOutputDTO;
|
||||||
|
import cc.ryanc.halo.model.entity.User;
|
||||||
import cc.ryanc.halo.model.vo.CommentVO;
|
import cc.ryanc.halo.model.vo.CommentVO;
|
||||||
import cc.ryanc.halo.service.AttachmentService;
|
import cc.ryanc.halo.service.*;
|
||||||
import cc.ryanc.halo.service.CommentService;
|
|
||||||
import cc.ryanc.halo.service.LogService;
|
|
||||||
import cc.ryanc.halo.service.PostService;
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin controller
|
* Admin controller
|
||||||
|
@ -31,14 +29,18 @@ public class AdminController {
|
||||||
|
|
||||||
private final LogService logService;
|
private final LogService logService;
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
public AdminController(PostService postService,
|
public AdminController(PostService postService,
|
||||||
CommentService commentService,
|
CommentService commentService,
|
||||||
AttachmentService attachmentService,
|
AttachmentService attachmentService,
|
||||||
LogService logService) {
|
LogService logService,
|
||||||
|
UserService userService) {
|
||||||
this.postService = postService;
|
this.postService = postService;
|
||||||
this.commentService = commentService;
|
this.commentService = commentService;
|
||||||
this.attachmentService = attachmentService;
|
this.attachmentService = attachmentService;
|
||||||
this.logService = logService;
|
this.logService = logService;
|
||||||
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,7 +49,7 @@ public class AdminController {
|
||||||
* @param model model
|
* @param model model
|
||||||
* @return template path: admin/admin_index.ftl
|
* @return template path: admin/admin_index.ftl
|
||||||
*/
|
*/
|
||||||
@GetMapping(value = {"", "/index"})
|
@GetMapping(value = {"", "index"})
|
||||||
public String admin(Model model) {
|
public String admin(Model model) {
|
||||||
|
|
||||||
Page<PostSimpleOutputDTO> postPage = postService.listLatest(5);
|
Page<PostSimpleOutputDTO> postPage = postService.listLatest(5);
|
||||||
|
@ -67,9 +69,14 @@ public class AdminController {
|
||||||
return "admin/admin_index";
|
return "admin/admin_index";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/login")
|
@PostMapping("login")
|
||||||
public String login(){
|
@ResponseBody
|
||||||
|
public UserOutputDTO login(@RequestParam("username") String username,
|
||||||
|
@RequestParam("password") String password) {
|
||||||
|
// Login
|
||||||
|
User user = userService.login(username, password);
|
||||||
|
|
||||||
return "";
|
// Convert and return
|
||||||
|
return new UserOutputDTO().convertFrom(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue