Halo(八)

欢迎光临我的博客[http://poetize.cn],前端使用Vue2,聊天室使用Vue3,后台使用Spring Boot

安全模块

用户描述类

/**
 * 基本 Entity
 */
@Data
@MappedSuperclass
public class BaseEntity {

    /** Create time */
    @Column(name = "create_time", columnDefinition = "timestamp default CURRENT_TIMESTAMP")
    @Temporal(TemporalType.TIMESTAMP)
    private Date createTime;

    /** Update time */
    @Column(name = "update_time", columnDefinition = "timestamp default CURRENT_TIMESTAMP")
    @Temporal(TemporalType.TIMESTAMP)
    private Date updateTime;

    /** Delete flag */
    @Column(name = "deleted", columnDefinition = "TINYINT default 0")
    private Boolean deleted = false;

    /** 保存前被调用,初始化部分属性 */
    @PrePersist
    protected void prePersist() {
        deleted = false;
        Date now = DateUtils.now();
        if (createTime == null) {
            createTime = now;
        }

        if (updateTime == null) {
            updateTime = now;
        }
    }

    /** 更新前调用 */
    @PreUpdate
    protected void preUpdate() {
        updateTime = new Date();
    }

    /** 删除前调用 */
    @PreRemove
    protected void preRemove() {
        updateTime = new Date();
    }
}


/**
 * 用户 Entity
 */
@Data
@Entity
@Table(name = "users")
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class User extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "username", columnDefinition = "varchar(50) not null")
    private String username;

    @Column(name = "nickname", columnDefinition = "varchar(255) not null")
    private String nickname;

    @Column(name = "password", columnDefinition = "varchar(255) not null")
    private String password;

    @Column(name = "email", columnDefinition = "varchar(127) default ''")
    private String email;

    /** 头像 */
    @Column(name = "avatar", columnDefinition = "varchar(1023) default ''")
    private String avatar;

    /** 描述 */
    @Column(name = "description", columnDefinition = "varchar(1023) default ''")
    private String description;

    /** 生效时间(何时可以使用)*/
    @Column(name = "expire_time", columnDefinition = "timestamp default CURRENT_TIMESTAMP")
    @Temporal(TemporalType.TIMESTAMP)
    private Date expireTime;

    @Override
    public void prePersist() {
        super.prePersist();

        if (email == null) {
            email = "";
        }

        if (avatar == null) {
            avatar = "";
        }

        if (description == null) {
            description = "";
        }

        if (expireTime == null) {
            expireTime = DateUtils.now();	//立即生效
        }
    }
}


/**
 * 用户描述类(封装用户)
 */
@ToString
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
public class UserDetail {

    private User user;

    @NonNull
    public User getUser() {return user;}

    public void setUser(User user) {this.user = user;}
}

身份验证类

public interface Authentication {
    @NonNull
    UserDetail getDetail();
}

public class AuthenticationImpl implements Authentication {

    private final UserDetail userDetail;

    public AuthenticationImpl(UserDetail userDetail) {this.userDetail = userDetail;}

    @Override
    public UserDetail getDetail() {return userDetail;}
}

安全上下文类

public interface SecurityContext {

    @Nullable
    Authentication getAuthentication();

    void setAuthentication(@Nullable Authentication authentication);

    /** 检查是否已经验证过 */
    default boolean isAuthenticated() {return getAuthentication() != null;}
}

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

public class SecurityContextHolder {

    private final static ThreadLocal<SecurityContext> CONTEXT_HOLDER = new ThreadLocal<>();

    private SecurityContextHolder() {}

    /** 获取上下文 */
    @NonNull
    public static SecurityContext getContext() {
        SecurityContext context = CONTEXT_HOLDER.get();
        if (context == null) {
            context = createEmptyContext();
            CONTEXT_HOLDER.set(context);
        }
        return context;
    }

    /** 设置上下文 */
    public static void setContext(@Nullable SecurityContext context) {CONTEXT_HOLDER.set(context);}

    /** 清理上下文 */
    public static void clearContext() {CONTEXT_HOLDER.remove();}

    /** 创建空的安全上下文 */
    @NonNull
    private static SecurityContext createEmptyContext() {return new SecurityContextImpl(null);}
}

身份验证Token

@Data
public class AuthToken {

    /** 访问令牌 */
    @JsonProperty("access_token")
    private String accessToken;

    /** 过期时间 */
    @JsonProperty("expired_in")
    private int expiredIn;

    /** 刷新令牌 */
    @JsonProperty("refresh_token")
    private String refreshToken;
}

Token缓存Key

public class SecurityUtils {

    private SecurityUtils() {
    }

    //访问 Token Key(halo.admin.access_token.user.getId())
    @NonNull
    public static String buildAccessTokenKey(@NonNull User user) {
        return ACCESS_TOKEN_CACHE_PREFIX + user.getId();
    }

    //刷新 Token Key(halo.admin.refresh_token.user.getId())
    @NonNull
    public static String buildRefreshTokenKey(@NonNull User user) {
        return REFRESH_TOKEN_CACHE_PREFIX + user.getId();
    }

    // Token 访问 Key(halo.admin.access.token.accessToken)
    @NonNull
    public static String buildTokenAccessKey(@NonNull String accessToken) {
        return TOKEN_ACCESS_CACHE_PREFIX + accessToken;
    }

    // Token 刷新 Key(halo.admin.refresh_token.refreshToken)
    @NonNull
    public static String buildTokenRefreshKey(@NonNull String refreshToken) {
        return TOKEN_REFRESH_CACHE_PREFIX + refreshToken;
    }
}

SpringBoot自定义参数解析HandlerMethodArgumentResolver(实现HandlerMethodArgumentResolver接口)

public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver {

    public AuthenticationArgumentResolver() {
    }

    /**
     * 支持的参数(解析参数的类型:Authentication,UserDetail,User)
     */
    @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) {

        //获取参数类型
        Class<?> parameterType = parameter.getParameterType();

        //获取身份验证类(安全上下文没有身份验证类表示未登陆)
        Authentication authentication = Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
                .orElseThrow(() -> new AuthenticationException("未登陆!"));

        //返回相应类型参数值(从安全上下文获取的身份验证信息)
        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();
        }

        throw new UnsupportedOperationException("未知参数类型:" + parameterType);
    }
}


/**
 * SpringBoot注册自定义参数处理器
 */
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
	resolvers.add(new AuthenticationArgumentResolver());
}

OncePerRequestFilter:一次请求只过滤一次

/**
 * 身份验证过滤器抽象类
 */
public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter {

    protected final AntPathMatcher antPathMatcher;  //Url地址匹配工具类
    protected final HaloProperties haloProperties;  //配置文件实体类
    protected final OptionService optionService;    //选项操作
    private AuthenticationFailureHandler failureHandler;    //失败处理器
    /**
     * 排除Url模板
     */
    private Set<String> excludeUrlPatterns = new HashSet<>(2);


    protected AbstractAuthenticationFilter(HaloProperties haloProperties,
                                           OptionService optionService) {
        this.haloProperties = haloProperties;
        this.optionService = optionService;
        antPathMatcher = new AntPathMatcher();
    }


    /**
     * 从HttpServletRequest中获取Token
     */
    @Nullable
    protected abstract String getTokenFromRequest(@NonNull HttpServletRequest request);


    /** 身份验证 */
    protected abstract void doAuthenticate(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException;


    /** 不应该过滤的请求 */
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        //anyMatch(T -> boolean):是否有元素符合匹配条件
        return excludeUrlPatterns.stream().anyMatch(p -> antPathMatcher.match(p, request.getServletPath()));
    }


    /**
     * 添加排除Url模板
     *
     * String... excludeUrlPatterns:
     *      本质上可变参数是一个数组。
     *      一个方法只能有一个可变参数,并且需要放在最后。
     */
    public void addExcludeUrlPatterns(@NonNull String... excludeUrlPatterns) {
        Collections.addAll(this.excludeUrlPatterns, excludeUrlPatterns);
    }


    /**
     * 获取排除Url模板
     */
    @NonNull
    public Set<String> getExcludeUrlPatterns() {
        return excludeUrlPatterns;
    }


    /**
     * 得到失败处理器
     */
    @NonNull
    protected AuthenticationFailureHandler getFailureHandler() {
        if (failureHandler == null) {
            synchronized (this) {
                if (failureHandler == null) {
                    // Create default authentication failure handler
                    DefaultAuthenticationFailureHandler failureHandler = new DefaultAuthenticationFailureHandler();
                    failureHandler.setProductionEnv(haloProperties.isProductionEnv());

                    this.failureHandler = failureHandler;
                }
            }
        }
        return failureHandler;
    }


    /**
     * 设置失败处理器
     */
    public void setFailureHandler(@NonNull AuthenticationFailureHandler failureHandler) {
        this.failureHandler = failureHandler;
    }


    /** 过滤 */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //检查博客是否被安装
        Boolean isInstalled = optionService.getByPropertyOrDefault(PrimaryProperties.IS_INSTALLED, Boolean.class, false);

        if (!isInstalled) {
            getFailureHandler().onFailure(request, response, new NotInstallException("当前博客还没有初始化"));
            return;
        }

        if (shouldNotFilter(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        try {
            //身份验证
            doAuthenticate(request, response, filterChain);
        } finally {
            SecurityContextHolder.clearContext();
        }
    }
}

管理员身份验证过滤器

@Slf4j
public class AdminAuthenticationFilter extends AbstractAuthenticationFilter {

    /** Admin session key */
    public final static String ADMIN_SESSION_KEY = "halo.admin.session";

    /** Access token cache prefix */
    public final static String TOKEN_ACCESS_CACHE_PREFIX = "halo.admin.access.token.";

    /** Refresh token cache prefix */
    public final static String TOKEN_REFRESH_CACHE_PREFIX = "halo.admin.refresh.token.";

    /** 管理员Token请求头name */
    public final static String ADMIN_TOKEN_HEADER_NAME = "ADMIN-" + HttpHeaders.AUTHORIZATION;

    /** 管理员Token请求参数name */
    public final static String ADMIN_TOKEN_QUERY_NAME = "admin_token";

    private final HaloProperties haloProperties;

    private final StringCacheStore cacheStore;

    private final UserService userService;

    public AdminAuthenticationFilter(StringCacheStore cacheStore,
                                     UserService userService,
                                     HaloProperties haloProperties,
                                     OptionService optionService) {
        super(haloProperties, optionService);
        this.cacheStore = cacheStore;
        this.userService = userService;
        this.haloProperties = haloProperties;
    }


    /** 身份验证 */
    @Override
    protected void doAuthenticate(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        if (!haloProperties.isAuthEnabled()) {
            //如果当前用户存在,设置当前用户到安全上下文中,并通过过滤
            userService.getCurrentUser().ifPresent(user ->
                    SecurityContextHolder.setContext(new SecurityContextImpl(new AuthenticationImpl(new UserDetail(user)))));
            filterChain.doFilter(request, response);
            return;
        }

        //当前用户不存在,获取Token
        String token = getTokenFromRequest(request);

        //Token不存在,表示未登陆
        if (StringUtils.isBlank(token)) {
            getFailureHandler().onFailure(request, response, new AuthenticationException("未登录,请登陆后访问"));
            return;
        }

        //从缓存中获取User Id
        Optional<Integer> optionalUserId = cacheStore.getAny(SecurityUtils.buildTokenAccessKey(token), Integer.class);

        //没有User Id,表示Token过期
        if (!optionalUserId.isPresent()) {
            getFailureHandler().onFailure(request, response, new AuthenticationException("Token 已过期或不存在").setErrorData(token));
            return;
        }

        //获取User
        User user = userService.getById(optionalUserId.get());

        //构建 UserDetail
        UserDetail userDetail = new UserDetail(user);

        //设置当前用户到安全上下文中,并通过过滤
        SecurityContextHolder.setContext(new SecurityContextImpl(new AuthenticationImpl(userDetail)));
        filterChain.doFilter(request, response);
    }


    /** 获取Token */
    @Override
    protected String getTokenFromRequest(@NonNull HttpServletRequest request) {

        //从请求头中获取Token
        String token = request.getHeader(ADMIN_TOKEN_HEADER_NAME);

        //如果请求头中Token是null,从参数中获取Token
        if (StringUtils.isBlank(token)) {
            token = request.getParameter(ADMIN_TOKEN_QUERY_NAME);

            log.info("从参数中获取Token:[{}:{}]", ADMIN_TOKEN_QUERY_NAME, token);
        } else {
            log.info("从请求头中获取Token:[{}:{}]", ADMIN_TOKEN_HEADER_NAME, token);
        }
        return token;
    }
}

默认身份验证失败处理器

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;    //401:未授权
    }
}


@Slf4j
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private boolean productionEnv = true;   //生产环境(默认)

    //默认Json转换器
    private ObjectMapper objectMapper = JsonUtils.DEFAULT_JSON_MAPPER;

    public DefaultAuthenticationFailureHandler() {
    }


    @Override
    public void onFailure(HttpServletRequest request, HttpServletResponse response, HaloException exception) throws IOException, ServletException {
        log.warn("身份验证失败,ip:[{}]", ServletUtil.getClientIP(request));

        BaseResponse<Object> errorDetail = new BaseResponse<>();

        errorDetail.setStatus(exception.getStatus().value());
        errorDetail.setMessage(exception.getMessage());
        errorDetail.setData(exception.getErrorData());

        if (!productionEnv) {
            errorDetail.setDevMessage(ExceptionUtils.getStackTrace(exception));
        }

        //设置响应类型:"application/json;charset=UTF-8"
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        //设置状态码
        response.setStatus(exception.getStatus().value());
        //打印响应体到前端
        response.getWriter().write(objectMapper.writeValueAsString(errorDetail));
    }


    /**
     * 设置Json转换器
     */
    public void setObjectMapper(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    /**
     * 设置生产环境
     */
    public void setProductionEnv(boolean productionEnv) {
        this.productionEnv = productionEnv;
    }
}

注册过滤器

@Configuration
public class HaloConfiguration {

    @Bean
    public FilterRegistrationBean<AdminAuthenticationFilter> adminAuthenticationFilter(StringCacheStore cacheStore,
                                                                                       UserService userService,
                                                                                       HaloProperties haloProperties,
                                                                                       ObjectMapper objectMapper,
                                                                                       OptionService optionService) {
        AdminAuthenticationFilter adminAuthenticationFilter = new AdminAuthenticationFilter(cacheStore, 
                                userService, haloProperties, optionService);

        DefaultAuthenticationFailureHandler failureHandler = new DefaultAuthenticationFailureHandler();
        failureHandler.setProductionEnv(haloProperties.isProductionEnv());
        failureHandler.setObjectMapper(objectMapper);

        //排除路径
        adminAuthenticationFilter.addExcludeUrlPatterns(
                "/api/admin/login",
                "/api/admin/refresh/*",
                "/api/admin/installations",
                "/api/admin/recoveries/migrations/*",
                "/api/admin/is_installed",
                "/api/admin/password/code",
                "/api/admin/password/reset"
        );
        adminAuthenticationFilter.setFailureHandler(
                failureHandler);

        FilterRegistrationBean<AdminAuthenticationFilter> authenticationFilter = new FilterRegistrationBean<>();
        authenticationFilter.setFilter(adminAuthenticationFilter);
        authenticationFilter.addUrlPatterns("/api/admin/*");
        authenticationFilter.setOrder(1);

        return authenticationFilter;
    }
}
posted @ 2019-11-27 10:04  LittleDonkey  阅读(339)  评论(0编辑  收藏  举报