2. springsecurity oauth2 資源服務(wù)配置 版本2.2.2

1. @EnableResourceServer 注解
  • @EnableResourceServer 配置接口為ResourceServerConfigurer,接口內(nèi)容如下
public interface ResourceServerConfigurer {

    /**
     * Add resource-server specific properties (like a resource id). The defaults should work for many applications, but
     * you might want to change at least the resource id.
     * 
     * @param resources configurer for the resource server
     * @throws Exception if there is a problem
     */
    void configure(ResourceServerSecurityConfigurer resources) throws Exception;

    /**
     * Use this to configure the access rules for secure resources. By default all resources <i>not</i> in "/oauth/**"
     * are protected (but no specific rules about scopes are given, for instance). You also get an
     * {@link OAuth2WebSecurityExpressionHandler} by default.
     * 
     * @param http the current http filter configuration
     * @throws Exception if there is a problem
     */
    void configure(HttpSecurity http) throws Exception;

}

其默認(rèn)實(shí)現(xiàn)為OAuth2ResourceServerConfiguration

@Configuration
@Conditional({OAuth2ResourceServerConfiguration.ResourceServerCondition.class})
@ConditionalOnClass({EnableResourceServer.class, SecurityProperties.class})
@ConditionalOnWebApplication
@ConditionalOnBean({ResourceServerConfiguration.class})
@Import({ResourceServerTokenServicesConfiguration.class})
public class OAuth2ResourceServerConfiguration {
    private final ResourceServerProperties resource;

    public OAuth2ResourceServerConfiguration(ResourceServerProperties resource) {
        this.resource = resource;
    }

    @Bean
    @ConditionalOnMissingBean({ResourceServerConfigurer.class})
    public ResourceServerConfigurer resourceServer() {
        return new OAuth2ResourceServerConfiguration.ResourceSecurityConfigurer(this.resource);
    }

    @ConditionalOnBean({AuthorizationServerEndpointsConfiguration.class})
    private static class AuthorizationServerEndpointsConfigurationBeanCondition {
        private AuthorizationServerEndpointsConfigurationBeanCondition() {
        }

        public static boolean matches(ConditionContext context) {
            Class<OAuth2ResourceServerConfiguration.AuthorizationServerEndpointsConfigurationBeanCondition> type = OAuth2ResourceServerConfiguration.AuthorizationServerEndpointsConfigurationBeanCondition.class;
            Conditional conditional = (Conditional)AnnotationUtils.findAnnotation(type, Conditional.class);
            StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(type);
            Class[] var4 = conditional.value();
            int var5 = var4.length;

            for(int var6 = 0; var6 < var5; ++var6) {
                Class<? extends Condition> conditionType = var4[var6];
                Condition condition = (Condition)BeanUtils.instantiateClass(conditionType);
                if (condition.matches(context, metadata)) {
                    return true;
                }
            }

            return false;
        }
    }

    protected static class ResourceServerCondition extends SpringBootCondition implements ConfigurationCondition {
        private static final Bindable<Map<String, Object>> STRING_OBJECT_MAP = Bindable.mapOf(String.class, Object.class);
        private static final String AUTHORIZATION_ANNOTATION = "org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration";

        protected ResourceServerCondition() {
        }

        public ConfigurationPhase getConfigurationPhase() {
            return ConfigurationPhase.REGISTER_BEAN;
        }

        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
            Builder message = ConditionMessage.forCondition("OAuth ResourceServer Condition", new Object[0]);
            Environment environment = context.getEnvironment();
            if (!(environment instanceof ConfigurableEnvironment)) {
                return ConditionOutcome.noMatch(message.didNotFind("A ConfigurableEnvironment").atAll());
            } else if (this.hasOAuthClientId(environment)) {
                return ConditionOutcome.match(message.foundExactly("client-id property"));
            } else {
                Binder binder = Binder.get(environment);
                String prefix = "security.oauth2.resource.";
                if (binder.bind(prefix + "jwt", STRING_OBJECT_MAP).isBound()) {
                    return ConditionOutcome.match(message.foundExactly("JWT resource configuration"));
                } else if (binder.bind(prefix + "jwk", STRING_OBJECT_MAP).isBound()) {
                    return ConditionOutcome.match(message.foundExactly("JWK resource configuration"));
                } else if (StringUtils.hasText(environment.getProperty(prefix + "user-info-uri"))) {
                    return ConditionOutcome.match(message.foundExactly("user-info-uri property"));
                } else if (StringUtils.hasText(environment.getProperty(prefix + "token-info-uri"))) {
                    return ConditionOutcome.match(message.foundExactly("token-info-uri property"));
                } else {
                    return ClassUtils.isPresent("org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration", (ClassLoader)null) && OAuth2ResourceServerConfiguration.AuthorizationServerEndpointsConfigurationBeanCondition.matches(context) ? ConditionOutcome.match(message.found("class").items(new Object[]{"org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration"})) : ConditionOutcome.noMatch(message.didNotFind("client ID, JWT resource or authorization server").atAll());
                }
            }
        }

        private boolean hasOAuthClientId(Environment environment) {
            return StringUtils.hasLength(environment.getProperty("security.oauth2.client.client-id"));
        }
    }

    protected static class ResourceSecurityConfigurer extends ResourceServerConfigurerAdapter {
        private ResourceServerProperties resource;

        public ResourceSecurityConfigurer(ResourceServerProperties resource) {
            this.resource = resource;
        }

        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId(this.resource.getResourceId());
        }

        public void configure(HttpSecurity http) throws Exception {
            ((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
        }
    }
}

我們可以提供自己的配置,如下所示

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    @Autowired
    protected ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
    @Autowired
    protected PlatformAccessDeniedHandler platformAccessDeniedHandler;
    @Autowired
    protected RemoteTokenServices remoteTokenServices;
    @Autowired
    protected UserDetailsService userDetailsService;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/actuator/**"
                , "/v2/api-docs").permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable();
    }
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
        DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
        userTokenConverter.setUserDetailsService(userDetailsService);
        accessTokenConverter.setUserTokenConverter(userTokenConverter);
        remoteTokenServices.setRestTemplate(lbRestTemplate());
        remoteTokenServices.setAccessTokenConverter(accessTokenConverter);
        resources.authenticationEntryPoint(resourceAuthExceptionEntryPoint)
                .accessDeniedHandler(platformAccessDeniedHandler)
                .tokenServices(remoteTokenServices);
    }
    @Bean
    @LoadBalanced
    public RestTemplate lbRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        //設(shè)置自定義異常處理
        restTemplate.setErrorHandler(new PlatformResponseErrorHandler());
        return restTemplate;
    }

}

ResourceServerSecurityConfigurer 在方法configure(HttpSecurity http) 配置了OAuth2AuthenticationProcessingFilter過濾器呈础,代碼如下

@Override
    public void configure(HttpSecurity http) throws Exception {

        AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
        resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
        resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
        resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
        if (eventPublisher != null) {
            resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
        }
        if (tokenExtractor != null) {
            resourcesServerFilter.setTokenExtractor(tokenExtractor);
        }
        resourcesServerFilter = postProcess(resourcesServerFilter);
        resourcesServerFilter.setStateless(stateless);

        // @formatter:off
        http
            .authorizeRequests().expressionHandler(expressionHandler)
        .and()
            .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint);
        // @formatter:on
    }
2.資源認(rèn)證的核心 OAuth2AuthenticationProcessingFilter過濾器逊拍。

來看一下它的源碼

public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {

    private final static Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class);

    private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();

    private AuthenticationManager authenticationManager;

    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();

    private TokenExtractor tokenExtractor = new BearerTokenExtractor();

    private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();

    private boolean stateless = true;

    /**
     * Flag to say that this filter guards stateless resources (default true). Set this to true if the only way the
     * resource can be accessed is with a token. If false then an incoming cookie can populate the security context and
     * allow access to a caller that isn't an OAuth2 client.
     * 
     * @param stateless the flag to set (default true)
     */
    public void setStateless(boolean stateless) {
        this.stateless = stateless;
    }

    /**
     * @param authenticationEntryPoint the authentication entry point to set
     */
    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    /**
     * @param authenticationManager the authentication manager to set (mandatory with no default)
     */
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    /**
     * @param tokenExtractor the tokenExtractor to set
     */
    public void setTokenExtractor(TokenExtractor tokenExtractor) {
        this.tokenExtractor = tokenExtractor;
    }

    /**
     * @param eventPublisher the event publisher to set
     */
    public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    /**
     * @param authenticationDetailsSource The AuthenticationDetailsSource to use
     */
    public void setAuthenticationDetailsSource(
            AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
        Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
        this.authenticationDetailsSource = authenticationDetailsSource;
    }

    public void afterPropertiesSet() {
        Assert.state(authenticationManager != null, "AuthenticationManager is required");
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
            ServletException {

        final boolean debug = logger.isDebugEnabled();
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;

        try {

            Authentication authentication = tokenExtractor.extract(request);
            
            if (authentication == null) {
                if (stateless && isAuthenticated()) {
                    if (debug) {
                        logger.debug("Clearing security context.");
                    }
                    SecurityContextHolder.clearContext();
                }
                if (debug) {
                    logger.debug("No token in request, will continue chain.");
                }
            }
            else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
                if (authentication instanceof AbstractAuthenticationToken) {
                    AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
                    needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
                }
                Authentication authResult = authenticationManager.authenticate(authentication);

                if (debug) {
                    logger.debug("Authentication success: " + authResult);
                }

                eventPublisher.publishAuthenticationSuccess(authResult);
                SecurityContextHolder.getContext().setAuthentication(authResult);

            }
        }
        catch (OAuth2Exception failed) {
            SecurityContextHolder.clearContext();

            if (debug) {
                logger.debug("Authentication request failed: " + failed);
            }
            eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
                    new PreAuthenticatedAuthenticationToken("access-token", "N/A"));

            authenticationEntryPoint.commence(request, response,
                    new InsufficientAuthenticationException(failed.getMessage(), failed));

            return;
        }

        chain.doFilter(request, response);
    }

    private boolean isAuthenticated() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
            return false;
        }
        return true;
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void destroy() {
    }

    private static final class NullEventPublisher implements AuthenticationEventPublisher {
        public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
        }

        public void publishAuthenticationSuccess(Authentication authentication) {
        }
    }

}

當(dāng)access_token 不為空時(shí)裆甩,認(rèn)證管理器authenticationManager 即 OAuth2AuthenticationManager進(jìn)行身份認(rèn)證旱捧。身份認(rèn)證代碼如下:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        if (authentication == null) {
            throw new InvalidTokenException("Invalid token (token not found)");
        }
        String token = (String) authentication.getPrincipal();
        OAuth2Authentication auth = tokenServices.loadAuthentication(token);
        if (auth == null) {
            throw new InvalidTokenException("Invalid token: " + token);
        }

        Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
        if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
            throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
        }

        checkClientDetails(auth);

        if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
            OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
            // Guard against a cached copy of the same details
            if (!details.equals(auth.getDetails())) {
                // Preserve the authentication details from the one loaded by token services
                details.setDecodedDetails(auth.getDetails());
            }
        }
        auth.setDetails(authentication.getDetails());
        auth.setAuthenticated(true);
        return auth;

    }

tokenServices(實(shí)現(xiàn)類為RemoteTokenServices) 調(diào)用loadAuthentication(token) 方法進(jìn)行身份認(rèn)證

@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

    MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
    formData.add(tokenName, accessToken);
    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
    Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);

    if (map.containsKey("error")) {
        if (logger.isDebugEnabled()) {
            logger.debug("check_token returned error: " + map.get("error"));
        }
        throw new InvalidTokenException(accessToken);
    }

    // gh-838
    if (!Boolean.TRUE.equals(map.get("active"))) {
        logger.debug("check_token returned active attribute: " + map.get("active"));
        throw new InvalidTokenException(accessToken);
    }

    return tokenConverter.extractAuthentication(map);
}
... 省略其它方法 ...
private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
        if (headers.getContentType() == null) {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        }
        @SuppressWarnings("rawtypes")
        Map map = restTemplate.exchange(path, HttpMethod.POST,
                new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
        @SuppressWarnings("unchecked")
        Map<String, Object> result = map;
        return result;
    }

跟蹤代碼,我們可以看到最終是使用 restTemplatecheckTokenEndpointUrl 進(jìn)行認(rèn)證焕议,checkTokenEndpointUrl 的值是我們?cè)谂渲梦募信渲玫?token-info-uri

image.png

接下來便進(jìn)入 CheckTokenEndpoint 完成校驗(yàn)

@FrameworkEndpoint
public class CheckTokenEndpoint {
    private ResourceServerTokenServices resourceServerTokenServices;
    private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
    protected final Log logger = LogFactory.getLog(this.getClass());
    private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();

    public CheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) {
        this.resourceServerTokenServices = resourceServerTokenServices;
    }

    public void setExceptionTranslator(WebResponseExceptionTranslator exceptionTranslator) {
        this.exceptionTranslator = exceptionTranslator;
    }

    public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
        this.accessTokenConverter = accessTokenConverter;
    }

    @RequestMapping({"/oauth/check_token"})
    @ResponseBody
    public Map<String, ?> checkToken(@RequestParam("token") String value) {
        OAuth2AccessToken token = this.resourceServerTokenServices.readAccessToken(value);
        if (token == null) {
            throw new InvalidTokenException("Token was not recognised");
        } else if (token.isExpired()) {
            throw new InvalidTokenException("Token has expired");
        } else {
            OAuth2Authentication authentication = this.resourceServerTokenServices.loadAuthentication(token.getValue());
            Map<String, Object> response = this.accessTokenConverter.convertAccessToken(token, authentication);
            response.put("active", true);
            return response;
        }
    }

    @ExceptionHandler({InvalidTokenException.class})
    public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
        this.logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
        InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) {
            public int getHttpErrorCode() {
                return 400;
            }
        };
        return this.exceptionTranslator.translate(e400);
    }
}

至此贾陷,資源服務(wù)器主要內(nèi)容全部完成。

PS:
本人發(fā)現(xiàn) CheckTokenEndpoint 認(rèn)證失敗時(shí)會(huì)拋出異常卿堂,restTemplate 調(diào)用 check_token 時(shí)束莫,如果使用默認(rèn)的異常處理類DefaultResponseErrorHandler異常處理邏輯如下圖所示,會(huì)繼續(xù)拋出異常御吞,導(dǎo)致調(diào)用端出現(xiàn)500錯(cuò)誤麦箍。

protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
        switch (statusCode.series()) {
            case CLIENT_ERROR:
                throw new HttpClientErrorException(statusCode, response.getStatusText(),
                        response.getHeaders(), getResponseBody(response), getCharset(response));
            case SERVER_ERROR:
                throw new HttpServerErrorException(statusCode, response.getStatusText(),
                        response.getHeaders(), getResponseBody(response), getCharset(response));
            default:
                throw new UnknownHttpStatusCodeException(statusCode.value(), response.getStatusText(),
                        response.getHeaders(), getResponseBody(response), getCharset(response));
        }
    }

可以通過 restTemplate.setErrorHandler(new PlatformResponseErrorHandler()); 設(shè)置自定義異常處理類來進(jìn)行異常處理。

@Slf4j
public class PlatformResponseErrorHandler extends DefaultResponseErrorHandler {

    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException{
        return super.hasError(response);
    }

    @Override
    protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
        log.error("RestTemplate 異常信息 statusCode={}, response={}",statusCode, response.toString());
//        super.handleError(response, statusCode);
    }
}

以上僅僅是個(gè)人的一些理解及查看的源碼陶珠,如果有錯(cuò)誤或不足挟裂,歡迎指正!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末揍诽,一起剝皮案震驚了整個(gè)濱河市诀蓉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌暑脆,老刑警劉巖渠啤,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異添吗,居然都是意外死亡沥曹,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門碟联,熙熙樓的掌柜王于貴愁眉苦臉地迎上來妓美,“玉大人,你說我怎么就攤上這事鲤孵『埃” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵普监,是天一觀的道長(zhǎng)贵试。 經(jīng)常有香客問我琉兜,道長(zhǎng),這世上最難降的妖魔是什么毙玻? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任豌蟋,我火速辦了婚禮,結(jié)果婚禮上桑滩,老公的妹妹穿的比我還像新娘夺饲。我一直安慰自己,他們只是感情好施符,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布往声。 她就那樣靜靜地躺著,像睡著了一般戳吝。 火紅的嫁衣襯著肌膚如雪浩销。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天听哭,我揣著相機(jī)與錄音慢洋,去河邊找鬼。 笑死陆盘,一個(gè)胖子當(dāng)著我的面吹牛普筹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播隘马,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼太防,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了酸员?” 一聲冷哼從身側(cè)響起蜒车,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎幔嗦,沒想到半個(gè)月后酿愧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡邀泉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年嬉挡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汇恤。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡庞钢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屁置,到底是詐尸還是另有隱情焊夸,我是刑警寧澤仁连,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布蓝角,位于F島的核電站阱穗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏使鹅。R本人自食惡果不足惜揪阶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望患朱。 院中可真熱鬧鲁僚,春花似錦、人聲如沸裁厅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽执虹。三九已至拓挥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間袋励,已是汗流浹背侥啤。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留茬故,地道東北人盖灸。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像磺芭,于是被迫代替她去往敵國(guó)和親赁炎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353