zoukankan      html  css  js  c++  java
  • Halo(八)

    安全模块

    用户描述类

    /**
     * 基本 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;
        }
    }
    
  • 相关阅读:
    prase arraylist where in to ParamsQuery on sql server 2008 using TVB: tvpdemo.cs(where in 转sql 参数化查询)
    手动依赖性注入 NInject(1) (转载)
    单件(单态,Singleton)模式部分
    详解Javascript中的Url编码/解码
    学习Web应用漏洞最好的教程WebGoat(转载)
    百度网盘 邀请码
    colorbox去除close关闭按钮,附上colorbox的基本使用方法
    P3974 [TJOI2015]组合数学
    P1772 [ZJOI2006]物流运输
    P1434 [SHOI2002]滑雪
  • 原文地址:https://www.cnblogs.com/loveer/p/11940057.html
Copyright © 2011-2022 走看看