Add security module

pull/137/head
johnniang 2019-03-14 21:33:06 +08:00
parent c9c68eef54
commit f7b90652ba
19 changed files with 522 additions and 49 deletions

View File

@ -3,6 +3,11 @@ package cc.ryanc.halo.config;
import cc.ryanc.halo.config.properties.HaloProperties;
import cc.ryanc.halo.filter.CorsFilter;
import cc.ryanc.halo.filter.LogFilter;
import cc.ryanc.halo.security.filter.AdminAuthenticationFilter;
import cc.ryanc.halo.security.filter.ApiAuthenticationFilter;
import cc.ryanc.halo.security.handler.AdminAuthenticationFailureHandler;
import cc.ryanc.halo.security.handler.DefaultAuthenticationFailureHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@ -49,4 +54,30 @@ public class HaloConfiguration {
return logFilter;
}
@Bean
public FilterRegistrationBean<ApiAuthenticationFilter> apiAuthenticationFilter(HaloProperties haloProperties, ObjectMapper objectMapper) {
ApiAuthenticationFilter apiFilter = new ApiAuthenticationFilter();
// Set failure handler
apiFilter.setFailureHandler(new DefaultAuthenticationFailureHandler(haloProperties.getProductionEnv(), objectMapper));
FilterRegistrationBean<ApiAuthenticationFilter> authenticationFilter = new FilterRegistrationBean<>();
authenticationFilter.setFilter(apiFilter);
authenticationFilter.addUrlPatterns("/api/*");
authenticationFilter.setOrder(0);
return authenticationFilter;
}
@Bean
public FilterRegistrationBean<AdminAuthenticationFilter> adminAuthenticationFilter(HaloProperties haloProperties, ObjectMapper objectMapper) {
AdminAuthenticationFilter adminFilter = new AdminAuthenticationFilter();
// Set failure handler
adminFilter.setFailureHandler(new AdminAuthenticationFailureHandler(haloProperties.getProductionEnv(), objectMapper));
FilterRegistrationBean<AdminAuthenticationFilter> authenticationFilter = new FilterRegistrationBean<>();
authenticationFilter.setFilter(adminFilter);
authenticationFilter.addUrlPatterns("/admin/*");
authenticationFilter.setOrder(1);
return authenticationFilter;
}
}

View File

@ -1,20 +1,22 @@
package cc.ryanc.halo.config;
import cc.ryanc.halo.config.properties.HaloProperties;
import cc.ryanc.halo.security.resolver.AuthenticationArgumentResolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.List;
import java.util.Locale;
/**
@ -32,53 +34,13 @@ import java.util.Locale;
@PropertySource(value = "classpath:application.yaml", ignoreResourceNotFound = true, encoding = "UTF-8")
public class WebMvcAutoConfiguration implements WebMvcConfigurer {
// @Autowired
// private LoginInterceptor loginInterceptor;
//
// @Autowired
// private InstallInterceptor installInterceptor;
//
// @Autowired
// private ApiInterceptor apiInterceptor;
//
// @Autowired
// private LocaleInterceptor localeInterceptor;
//
@Autowired
private HaloProperties haloProperties;
//
// /**
// * 注册拦截器
// *
// * @param registry registry
// */
// @Override
// public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(loginInterceptor)
// .addPathPatterns("/admin.*")
// .addPathPatterns("/admin/**")
// .addPathPatterns("/backup/**")
// .excludePathPatterns("/admin/login")
// .excludePathPatterns("/admin/getLogin")
// .excludePathPatterns("/admin/findPassword")
// .excludePathPatterns("/admin/sendResetPasswordEmail")
// .excludePathPatterns("/admin/toResetPassword")
// .excludePathPatterns("/admin/resetPassword")
// .excludePathPatterns("/static/**");
// registry.addInterceptor(installInterceptor)
// .addPathPatterns("/**")
// .excludePathPatterns("/install")
// .excludePathPatterns("/install/do")
// .excludePathPatterns("/static/**");
// registry.addInterceptor(apiInterceptor)
// .addPathPatterns("/api/**");
// registry.addInterceptor(localeInterceptor)
// .addPathPatterns("/admin.*")
// .addPathPatterns("/admin/**")
// .addPathPatterns("/install");
// registry.addInterceptor(localeChangeInterceptor())
// .addPathPatterns("/install");
// }
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthenticationArgumentResolver());
}
/**
*

View File

@ -16,4 +16,9 @@ public class HaloProperties {
* Doc api disabled. (Default is true)
*/
private Boolean docDisabled = true;
/**
* Production env. (Default is true)
*/
private Boolean productionEnv = true;
}

View File

@ -0,0 +1,24 @@
package cc.ryanc.halo.exception;
import org.springframework.http.HttpStatus;
/**
* Authentication exception.
*
* @author johnniang
*/
public class AuthenticationException extends HaloException {
public AuthenticationException(String message) {
super(message);
}
public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
@Override
public HttpStatus getStatus() {
return HttpStatus.UNAUTHORIZED;
}
}

View File

@ -8,7 +8,7 @@ import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* Page with title only dto.
* Post minimal output dto.
*
* @author johnniang
*/

View File

@ -0,0 +1,25 @@
package cc.ryanc.halo.model.support;
import lombok.*;
/**
* Global response entity.
*
* @author johnniang
*/
@Data
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {
private Integer status;
private String message;
private String devMessage;
private Object data;
}

View File

@ -0,0 +1,20 @@
package cc.ryanc.halo.security.authentication;
import cc.ryanc.halo.security.support.UserDetail;
import org.springframework.lang.NonNull;
/**
* Authentication.
*
* @author johnniang
*/
public interface Authentication {
/**
* Get user detail.
*
* @return user detail
*/
@NonNull
UserDetail getDetail();
}

View File

@ -0,0 +1,22 @@
package cc.ryanc.halo.security.authentication;
import cc.ryanc.halo.security.support.UserDetail;
/**
* Authentication implementation.
*
* @author johnniang
*/
public class AuthenticationImpl implements Authentication {
private final UserDetail userDetail;
public AuthenticationImpl(UserDetail userDetail) {
this.userDetail = userDetail;
}
@Override
public UserDetail getDetail() {
return userDetail;
}
}

View File

@ -0,0 +1,27 @@
package cc.ryanc.halo.security.context;
import cc.ryanc.halo.security.authentication.Authentication;
import org.springframework.lang.Nullable;
/**
* Security context interface.
*
* @author johnniang
*/
public interface SecurityContext {
/**
* Gets the currently authenticated principal.
*
* @return the Authentication or null if authentication information is unavailable
*/
@Nullable
Authentication getAuthentication();
/**
* Changes the currently authenticated principal, or removes the authentication information.
*
* @param authentication the new authentication or null if no further authentication should not be stored
*/
void setAuthentication(@Nullable Authentication authentication);
}

View File

@ -0,0 +1,63 @@
package cc.ryanc.halo.security.context;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Security context holder.
*
* @author johnniang
* @date 12/11/18
*/
public class SecurityContextHolder {
private final static ThreadLocal<SecurityContext> CONTEXT_HOLDER = new ThreadLocal<>();
private SecurityContextHolder() {
}
/**
* Gets context.
*
* @return security context
*/
@NonNull
public static SecurityContext getContext() {
// Get from thread local
SecurityContext context = CONTEXT_HOLDER.get();
if (context == null) {
// If no context is available now then create an empty context
context = createEmptyContext();
// Set to thread local
CONTEXT_HOLDER.set(context);
}
return context;
}
/**
* Sets security context.
*
* @param context security context
*/
public static void setContext(@Nullable SecurityContext context) {
CONTEXT_HOLDER.set(context);
}
/**
* Clears context.
*/
public static void clearContext() {
CONTEXT_HOLDER.remove();
}
/**
* Creates an empty security context.
*
* @return an empty security context
*/
@NonNull
private static SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}

View File

@ -0,0 +1,32 @@
package cc.ryanc.halo.security.context;
import cc.ryanc.halo.security.authentication.Authentication;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* Security context implementation.
*
* @author johnniang
*/
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class SecurityContextImpl implements SecurityContext {
private Authentication authentication;
@Override
public Authentication getAuthentication() {
return authentication;
}
@Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
}

View File

@ -0,0 +1,29 @@
package cc.ryanc.halo.security.filter;
import cc.ryanc.halo.security.handler.AuthenticationFailureHandler;
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;
/**
* Admin authentication filter.
*
* @author johnniang
*/
public class AdminAuthenticationFilter extends OncePerRequestFilter {
private AuthenticationFailureHandler failureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// TODO Handle admin authentication
}
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
}
}

View File

@ -0,0 +1,30 @@
package cc.ryanc.halo.security.filter;
import cc.ryanc.halo.security.handler.AuthenticationFailureHandler;
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;
/**
* Api authentication Filter
*
* @author johnniang
*/
public class ApiAuthenticationFilter extends OncePerRequestFilter {
private AuthenticationFailureHandler failureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// TODO Handle authentication
filterChain.doFilter(request, response);
}
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
}
}

View File

@ -0,0 +1,26 @@
package cc.ryanc.halo.security.handler;
import cc.ryanc.halo.exception.HaloException;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Authentication failure handler.
*
* @author johnniang
*/
public class AdminAuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {
public AdminAuthenticationFailureHandler(boolean productionEnv, ObjectMapper objectMapper) {
super(productionEnv, objectMapper);
}
@Override
public void onFailure(HttpServletRequest request, HttpServletResponse response, HaloException exception) throws IOException, ServletException {
// TODO handler the admin authentication failure.
}
}

View File

@ -0,0 +1,27 @@
package cc.ryanc.halo.security.handler;
import cc.ryanc.halo.exception.HaloException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Authentication failure handler.
*
* @author johnniang
*/
public interface AuthenticationFailureHandler {
/**
* Calls when a user has been unsuccessfully authenticated.
*
* @param request http servlet request
* @param response http servlet response
* @param exception api exception
* @throws IOException io exception
* @throws ServletException service exception
*/
void onFailure(HttpServletRequest request, HttpServletResponse response, HaloException exception) throws IOException, ServletException;
}

View File

@ -0,0 +1,54 @@
package cc.ryanc.halo.security.handler;
import cc.ryanc.halo.exception.HaloException;
import cc.ryanc.halo.model.support.ErrorResponse;
import cc.ryanc.halo.utils.ExceptionUtils;
import cn.hutool.extra.servlet.ServletUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Default AuthenticationFailureHandler.
*
* @author johnniang
* @date 12/12/18
*/
@Slf4j
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final boolean productionEnv;
private final ObjectMapper objectMapper;
public DefaultAuthenticationFailureHandler(boolean productionEnv,
ObjectMapper objectMapper) {
this.productionEnv = productionEnv;
this.objectMapper = objectMapper;
}
@Override
public void onFailure(HttpServletRequest request, HttpServletResponse response, HaloException exception) throws IOException, ServletException {
log.warn("Handle unsuccessful authentication, ip: [{}]", ServletUtil.getClientIP(request));
ErrorResponse errorDetail = new ErrorResponse();
errorDetail.setMessage(exception.getMessage());
if (!productionEnv) {
errorDetail.setDevMessage(ExceptionUtils.getStackTrace(exception));
}
log.debug("Response error: [{}]", errorDetail);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(exception.getStatus().value());
response.getWriter().write(objectMapper.writeValueAsString(errorDetail));
}
}

View File

@ -0,0 +1,61 @@
package cc.ryanc.halo.security.resolver;
import cc.ryanc.halo.exception.AuthenticationException;
import cc.ryanc.halo.model.entity.User;
import cc.ryanc.halo.security.authentication.Authentication;
import cc.ryanc.halo.security.context.SecurityContextHolder;
import cc.ryanc.halo.security.support.UserDetail;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import java.util.Optional;
/**
* Authentication argument resolver.
*
* @author johnniang
* @date 12/11/18
*/
@Slf4j
public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver {
public AuthenticationArgumentResolver() {
log.debug("Initializing AuthenticationArgumentResolver");
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> parameterType = parameter.getParameterType();
return (Authentication.class.isAssignableFrom(parameterType) ||
UserDetail.class.isAssignableFrom(parameterType) ||
User.class.isAssignableFrom(parameterType));
}
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {
log.debug("Handle AuthenticationArgument");
Class<?> parameterType = parameter.getParameterType();
Authentication authentication = Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.orElseThrow(() -> new AuthenticationException("You haven't signed in yet"));
if (Authentication.class.isAssignableFrom(parameterType)) {
return authentication;
} else if (UserDetail.class.isAssignableFrom(parameterType)) {
return authentication.getDetail();
} else if (User.class.isAssignableFrom(parameterType)) {
return authentication.getDetail().getUser();
}
// Should never happen...
throw new UnsupportedOperationException("Unknown parameter type: " + parameterType);
}
}

View File

@ -0,0 +1,34 @@
package cc.ryanc.halo.security.support;
import cc.ryanc.halo.exception.AuthenticationException;
import cc.ryanc.halo.model.entity.User;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.lang.NonNull;
/**
* User detail.
*
* @author johnniang
*/
@ToString
@EqualsAndHashCode
public class UserDetail {
private User user;
/**
* Gets user info.
*
* @return user info
* @throws AuthenticationException throws if the user is null
*/
@NonNull
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}

View File

@ -8,6 +8,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.SortDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@ -40,12 +41,12 @@ public class PostController {
* @param status post status
* @param page current page
* @param sort sort
*
* @return template path: admin/admin_post.ftl
*/
@GetMapping
public String posts(Model model,
@RequestParam(value = "status", defaultValue = "0") PostStatus status,
@RequestParam(value = "status", defaultValue = "PUBLISHED") PostStatus status,
@PageableDefault Pageable defaultPageable,
@RequestParam(value = "page", defaultValue = "0") Integer page,
@SortDefault.SortDefaults({
@SortDefault(sort = "postPriority", direction = DESC),