Complete user login

pull/137/head
johnniang 2019-03-18 21:36:43 +08:00
parent 90b7007b6e
commit b547e0720b
7 changed files with 388 additions and 12 deletions

View File

@ -2,6 +2,9 @@ package cc.ryanc.halo.repository;
import cc.ryanc.halo.model.entity.User;
import cc.ryanc.halo.repository.base.BaseRepository;
import org.springframework.lang.NonNull;
import java.util.Optional;
/**
* User repository.
@ -10,4 +13,12 @@ import cc.ryanc.halo.repository.base.BaseRepository;
*/
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);
}

View File

@ -28,6 +28,11 @@ public class UserDetail {
return user;
}
/**
* Sets user info.
*
* @param user user info
*/
public void setUser(User user) {
this.user = user;
}

View File

@ -1,7 +1,11 @@
package cc.ryanc.halo.service;
import cc.ryanc.halo.exception.NotFoundException;
import cc.ryanc.halo.model.entity.User;
import cc.ryanc.halo.service.base.CrudService;
import org.springframework.lang.NonNull;
import java.util.Optional;
/**
* User service.
@ -10,4 +14,43 @@ import cc.ryanc.halo.service.base.CrudService;
*/
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);
}

View File

@ -1,10 +1,19 @@
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.repository.UserRepository;
import cc.ryanc.halo.service.UserService;
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.util.Assert;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* UserService implementation class
@ -17,8 +26,64 @@ public class UserServiceImpl extends AbstractCrudService<User, Integer> implemen
private final UserRepository userRepository;
public UserServiceImpl(UserRepository userRepository) {
private final StringCacheStore stringCacheStore;
public UserServiceImpl(UserRepository userRepository,
StringCacheStore stringCacheStore) {
super(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;
}
}

View File

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

View File

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

View File

@ -1,17 +1,15 @@
package cc.ryanc.halo.web.controller.admin;
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.entity.User;
import cc.ryanc.halo.model.vo.CommentVO;
import cc.ryanc.halo.service.AttachmentService;
import cc.ryanc.halo.service.CommentService;
import cc.ryanc.halo.service.LogService;
import cc.ryanc.halo.service.PostService;
import cc.ryanc.halo.service.*;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.*;
/**
* Admin controller
@ -31,14 +29,18 @@ public class AdminController {
private final LogService logService;
private final UserService userService;
public AdminController(PostService postService,
CommentService commentService,
AttachmentService attachmentService,
LogService logService) {
LogService logService,
UserService userService) {
this.postService = postService;
this.commentService = commentService;
this.attachmentService = attachmentService;
this.logService = logService;
this.userService = userService;
}
/**
@ -47,7 +49,7 @@ public class AdminController {
* @param model model
* @return template path: admin/admin_index.ftl
*/
@GetMapping(value = {"", "/index"})
@GetMapping(value = {"", "index"})
public String admin(Model model) {
Page<PostSimpleOutputDTO> postPage = postService.listLatest(5);
@ -67,9 +69,14 @@ public class AdminController {
return "admin/admin_index";
}
@GetMapping(value = "/login")
public String login(){
@PostMapping("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);
}
}