Spring Security項(xiàng)目第三方登陸(四)

OAuth協(xié)議

image.png

OAuth協(xié)議中的授權(quán)模式

  • 授權(quán)碼模式(authorization code)
  • 密碼模式(resource owner password credentials)
  • 客戶端模式(client credentials)
  • 簡(jiǎn)化模式(implicit)

授權(quán)碼模式

image.png

spring social基本原理

image.png
image.png

QQ登陸

返回User封裝好到對(duì)象

public class QQUserInfo  {
    /**
     *QQ
     */
    private String openId;

    private String constellation;

    private String is_lost;

    /**
     *  返回碼
     */
    private String ret;
    /**
     * 如果ret<0,會(huì)有相應(yīng)的錯(cuò)誤信息提示粥庄,返回?cái)?shù)據(jù)全部用UTF-8編碼丧失。
     */
    private String msg;

    /**
     *  用戶在QQ空間的昵稱。
     */
    private String nickname;
    /**
     * 出生年
     */
    private String year;

    /**
     * 性別默認(rèn)男
     */
    String gender;
    /**
     * 省
     */
    String province;

    /**
     * 市
     */
    private String city;

    private String figureurl_type;

    /**
     *  大小為30×30像素的QQ空間頭像URL惜互。
     */
    private String figureurl;
    /**
     *  大小為50×50像素的QQ空間頭像URL布讹。
     */
    private String figureurl_1;

    /**
     *  大小為100×100像素的QQ空間頭像URL琳拭。
     */
    private String figureurl_2;


    private String figureurl_qq;
    /**
     *  大小為40×40像素的QQ頭像URL。
     */
    private String figureurl_qq_1;
    /**
     *  大小為100×100像素的QQ頭像URL描验。需要注意白嘁,不是所有的用戶都擁有QQ的100×100的頭像,但40×40像素則是一定會(huì)有膘流。
     */
    private String figureurl_qq_2;

    /**
     *  標(biāo)識(shí)用戶是否為黃鉆用戶(0:不是絮缅;1:是)。
     */
    private String is_yellow_vip;
    /**
     *  標(biāo)識(shí)用戶是否為黃鉆用戶(0:不是呼股;1:是)
     */
    private String vip;
    /**
     *  黃鉆等級(jí)
     */
    private String yellow_vip_level;
    /**
     *  黃鉆等級(jí)
     */
    private String level;
    /**
     * 標(biāo)識(shí)是否為年費(fèi)黃鉆用戶(0:不是耕魄; 1:是)
     */
    private String is_yellow_year_vip;

    public String getOpenId() {
        return openId;
    }

    public void setOpenId(String openId) {
        this.openId = openId;
    }

    public String getIs_lost() {
        return is_lost;
    }

    public void setIs_lost(String is_lost) {
        this.is_lost = is_lost;
    }

    public String getRet() {
        return ret;
    }

    public void setRet(String ret) {
        this.ret = ret;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public String getYear() {
        return year;
    }

    public void setYear(String year) {
        this.year = year;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getProvince() {
        return province;
    }

    public void setProvince(String province) {
        this.province = province;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getFigureurl() {
        return figureurl;
    }

    public void setFigureurl(String figureurl) {
        this.figureurl = figureurl;
    }

    public String getFigureurl_1() {
        return figureurl_1;
    }

    public void setFigureurl_1(String figureurl_1) {
        this.figureurl_1 = figureurl_1;
    }

    public String getFigureurl_2() {
        return figureurl_2;
    }

    public void setFigureurl_2(String figureurl_2) {
        this.figureurl_2 = figureurl_2;
    }

    public String getFigureurl_qq() {
        return figureurl_qq;
    }

    public void setFigureurl_qq(String figureurl_qq) {
        this.figureurl_qq = figureurl_qq;
    }

    public String getFigureurl_qq_1() {
        return figureurl_qq_1;
    }

    public void setFigureurl_qq_1(String figureurl_qq_1) {
        this.figureurl_qq_1 = figureurl_qq_1;
    }

    public String getFigureurl_qq_2() {
        return figureurl_qq_2;
    }

    public void setFigureurl_qq_2(String figureurl_qq_2) {
        this.figureurl_qq_2 = figureurl_qq_2;
    }

    public String getIs_yellow_vip() {
        return is_yellow_vip;
    }

    public void setIs_yellow_vip(String is_yellow_vip) {
        this.is_yellow_vip = is_yellow_vip;
    }

    public String getVip() {
        return vip;
    }

    public void setVip(String vip) {
        this.vip = vip;
    }

    public String getYellow_vip_level() {
        return yellow_vip_level;
    }

    public void setYellow_vip_level(String yellow_vip_level) {
        this.yellow_vip_level = yellow_vip_level;
    }

    public String getLevel() {
        return level;
    }

    public void setLevel(String level) {
        this.level = level;
    }

    public String getIs_yellow_year_vip() {
        return is_yellow_year_vip;
    }

    public void setIs_yellow_year_vip(String is_yellow_year_vip) {
        this.is_yellow_year_vip = is_yellow_year_vip;
    }

    public String getConstellation() {
        return constellation;
    }

    public void setConstellation(String constellation) {
        this.constellation = constellation;
    }

    public String getFigureurl_type() {
        return figureurl_type;
    }

    public void setFigureurl_type(String figureurl_type) {
        this.figureurl_type = figureurl_type;
    }
public interface QQ {

    /**
     * 獲取用戶信息
     * @return
     */
    public QQUserInfo getUserInfo();
}
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
    private Logger logger= LoggerFactory.getLogger(getClass());

    /**
     * 獲取openId
     */
    private static final String URL_GET_OPENID="https://graph.qq.com/oauth2.0/me?access_token=%s";

    /**
     * 獲取用戶信息
     */
    private static final String URL_GET_USERINFO="https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";


    //QQ互聯(lián)應(yīng)用id
    private String appId;

    //QQ號(hào)碼
    private String openId;

    private ObjectMapper objectMapper=new ObjectMapper();


    public QQImpl(String accessToken,String appId){
        /**
         * TokenStrategy.ACCESS_TOKEN_PARAMETER 表示將accessToken放到傳入?yún)?shù)里面
         */
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
        this.appId=appId;

        String url=String.format(URL_GET_OPENID,accessToken);
        String result=getRestTemplate().getForObject(url,String.class);
        logger.info(result);
        this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
        logger.info("openid"+openId);

    }

    /**
     * 獲取用戶信息
     * @return
     * @throws IOException
     */
    @Override
    public QQUserInfo getUserInfo()  {
        String url=String.format(URL_GET_USERINFO,appId,openId);
        String result=getRestTemplate().getForObject(url,String.class);
        logger.info(result);
        QQUserInfo qqUserInfo=null;
        try {
             qqUserInfo=objectMapper.readValue(result,QQUserInfo.class);
             qqUserInfo.setOpenId(openId);
             return qqUserInfo;
        } catch (IOException e) {
            throw new RuntimeException("獲取用戶信息失敗");
        }

    }
}
@Configuration
//如果沒配置appId與appSecret擇配置類不生效
@ConditionalOnProperty(prefix = "guosh.security.social.qq",name = {"appId","appSecret"})
public class QQAutoConfig extends SocialAutoConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    @Override
    protected ConnectionFactory<?> createConnectionFactory() {
        QQProperties qqconfig= securityProperties.getSocial().getQq();
        //QQ連接工廠
        return new QQConnectionFactory(qqconfig.getProviderId(),qqconfig.getAppId(),qqconfig.getAppSecret());
    }
}
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {

    public QQConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new QQServerProvider(appId,appSecret), new QQAdapter());
    }
}

public class QQServerProvider extends AbstractOAuth2ServiceProvider<QQ> {

    private String appId;

    //導(dǎo)向認(rèn)證服務(wù)器
    private static final String URL_AUTHORIZE="https://graph.qq.com/oauth2.0/authorize";

    //申請(qǐng)令牌
    private static final String URL_ACCESS_TOKEN="https://graph.qq.com/oauth2.0/token";

    public QQServerProvider(String appId,String appSecret) {
        super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
        this.appId = appId;
    }

    @Override
    public QQ getApi(String accessToken) {
        return new QQImpl(accessToken,appId);
    }
}
public class QQOAuth2Template extends OAuth2Template {

    private Logger logger= LoggerFactory.getLogger(getClass());

    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        setUseParametersForClientAuthentication(true);
    }

    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate= super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }

    /**
     *處理返回的QQ token格式
     * @param accessTokenUrl
     * @param parameters
     * @return
     */
    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
        String responseStr= getRestTemplate().postForObject(accessTokenUrl,parameters,String.class);
        logger.info("獲取accessToken響應(yīng)"+responseStr);
        //分割字符串
        String[]items=StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr,"&");
        //token
        String accessToken = StringUtils.substringAfterLast(items[0], "=");
        Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
        //刷新令牌
        String refreshToken = StringUtils.substringAfterLast(items[2], "=");


        return new AccessGrant(accessToken,null,refreshToken,expiresIn);
    }
}
public class QQAdapter implements ApiAdapter<QQ> {

    //測(cè)試Api是否可用
    @Override
    public boolean test(QQ qq) {
        return true;
    }

    @Override
    public void setConnectionValues(QQ api, ConnectionValues values) {
        QQUserInfo userInfo=api.getUserInfo();
        //名稱
        values.setDisplayName(userInfo.getNickname());
        //頭像
        values.setImageUrl(userInfo.getFigureurl_qq());
        //主頁(yè)
        values.setProfileUrl(null);
        //openId
        values.setProviderUserId(userInfo.getOpenId());

    }

    @Override
    public UserProfile fetchUserProfile(QQ qq) {
        return null;
    }

    @Override
    public void updateStatus(QQ qq, String s) {
        //發(fā)送消息更新動(dòng)態(tài)
    }
}
public class MySpringSocialConfigurer extends SpringSocialConfigurer {
    private String filtertProcessesUrl;

    public MySpringSocialConfigurer(String filtertProcessesUrl) {
        this.filtertProcessesUrl = filtertProcessesUrl;
    }

    /**
     * 第三方認(rèn)證回調(diào)路徑
     * @param object
     * @param <T>
     * @return
     */
    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter filter= (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl(filtertProcessesUrl);
        return (T) filter;
    }
}
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private SecurityProperties securityProperties;


    @Autowired(required = false)
    private ConnectionSignUp connectionSignUp;

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        //把互聯(lián)數(shù)據(jù)存儲(chǔ)到表
        JdbcUsersConnectionRepository repository= new JdbcUsersConnectionRepository(dataSource,connectionFactoryLocator, Encryptors.noOpText());
        repository.setTablePrefix("sys_");
        //把qq讀區(qū)出來的數(shù)據(jù)自動(dòng)注冊(cè)
        if(connectionSignUp!=null){
            repository.setConnectionSignUp(connectionSignUp);
        }

        return repository;
    }

    @Bean
    public SpringSocialConfigurer sociaSecurityConfig(){
        String filtertProcessesUrl=securityProperties.getSocial().getFiltertProcessesUrl();
        MySpringSocialConfigurer configurer=new MySpringSocialConfigurer(filtertProcessesUrl);
        configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());//指定到注冊(cè)頁(yè)面
        return configurer;
    }
    //免注冊(cè)
    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){
        return new ProviderSignInUtils(connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator));
    }
}

微信登陸

public class WeixinUserInfo {

    /**
     * 普通用戶的標(biāo)識(shí),對(duì)當(dāng)前開發(fā)者帳號(hào)唯一
     */
    private String openid;
    /**
     * 普通用戶昵稱
     */
    private String nickname;
    /**
     * 語(yǔ)言
     */
    private String language;
    /**
     * 普通用戶性別彭谁,1為男性吸奴,2為女性
     */
    private String sex;
    /**
     * 普通用戶個(gè)人資料填寫的省份
     */
    private String province;
    /**
     * 普通用戶個(gè)人資料填寫的城市
     */
    private String city;
    /**
     * 國(guó)家,如中國(guó)為CN
     */
    private String country;
    /**
     * 用戶頭像缠局,最后一個(gè)數(shù)值代表正方形頭像大性虬隆(有0、46、64、96抬旺、132數(shù)值可選象对,0代表640*640正方形頭像),用戶沒有頭像時(shí)該項(xiàng)為空
     */
    private String headimgurl;
    /**
     * 用戶特權(quán)信息,json數(shù)組,如微信沃卡用戶為(chinaunicom)
     */
    private String[] privilege;
    /**
     * 用戶統(tǒng)一標(biāo)識(shí)。針對(duì)一個(gè)微信開放平臺(tái)帳號(hào)下的應(yīng)用馆匿,同一用戶的unionid是唯一的。
     */
    private String unionid;

    public String getOpenid() {
        return openid;
    }

    public void setOpenid(String openid) {
        this.openid = openid;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public String getLanguage() {
        return language;
    }

    public void setLanguage(String language) {
        this.language = language;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getProvince() {
        return province;
    }

    public void setProvince(String province) {
        this.province = province;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

    public String getHeadimgurl() {
        return headimgurl;
    }

    public void setHeadimgurl(String headimgurl) {
        this.headimgurl = headimgurl;
    }

    public String[] getPrivilege() {
        return privilege;
    }

    public void setPrivilege(String[] privilege) {
        this.privilege = privilege;
    }

    public String getUnionid() {
        return unionid;
    }

    public void setUnionid(String unionid) {
        this.unionid = unionid;
    }
}


public interface Weixin {
    /**
     * 獲取用戶信息
     * @return
     */
    public WeixinUserInfo getUserInfo(String openId);
}

public class WeixinImpl extends AbstractOAuth2ApiBinding implements Weixin{

    private Logger logger= LoggerFactory.getLogger(getClass());


    /**
     *
     */
    private ObjectMapper objectMapper = new ObjectMapper();
    /**
     * 獲取用戶信息的url
     */
    private static final String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";

    /**
     * @param accessToken
     */
    public WeixinImpl(String accessToken) {
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
    }

    /**
     * 默認(rèn)注冊(cè)的StringHttpMessageConverter字符集為ISO-8859-1燥滑,而微信返回的是UTF-8的渐北,所以覆蓋了原來的方法。
     */
    protected List<HttpMessageConverter<?>> getMessageConverters() {
        List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
        messageConverters.remove(0);
        messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return messageConverters;
    }

    /**
     * 用戶信息獲取
     * @param openId
     * @return
     */
    @Override
    public WeixinUserInfo getUserInfo(String openId) {
        String url = URL_GET_USER_INFO + openId;
        String response = getRestTemplate().getForObject(url, String.class);
        logger.info(response);
        if(StringUtils.contains(response, "errcode")) {
            return null;
        }
        WeixinUserInfo profile = null;
        try {
            profile = objectMapper.readValue(response, WeixinUserInfo.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return profile;
    }
}
public class WeixinAccessGrant extends AccessGrant {
    
    /**
     * 
     */
    private static final long serialVersionUID = -7243374526633186782L;
    
    private String openId;
    
    public WeixinAccessGrant() {
        super("");
    }

    public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {
        super(accessToken, scope, refreshToken, expiresIn);
    }

    /**
     * @return the openId
     */
    public String getOpenId() {
        return openId;
    }

    /**
     * @param openId the openId to set
     */
    public void setOpenId(String openId) {
        this.openId = openId;
    }
    
}
/**
 * 微信 api適配器铭拧,將微信 api的數(shù)據(jù)模型轉(zhuǎn)為spring social的標(biāo)準(zhǔn)模型赃蛛。
 * 
 * 
 * @author guosh
 *
 */
public class WeixinAdapter implements ApiAdapter<Weixin> {
    
    private String openId;
    
    public WeixinAdapter() {}
    
    public WeixinAdapter(String openId){
        this.openId = openId;
    }

    /**
     * @param api
     * @return
     */
    @Override
    public boolean test(Weixin api) {
        return true;
    }

    /**
     * @param api
     * @param values
     */
    @Override
    public void setConnectionValues(Weixin api, ConnectionValues values) {
        WeixinUserInfo profile = api.getUserInfo(openId);
        values.setProviderUserId(profile.getOpenid());
        values.setDisplayName(profile.getNickname());
        values.setImageUrl(profile.getHeadimgurl());
    }

    /**
     * @param api
     * @return
     */
    @Override
    public UserProfile fetchUserProfile(Weixin api) {
        return null;
    }

    /**
     * @param api
     * @param message
     */
    @Override
    public void updateStatus(Weixin api, String message) {
        //do nothing
    }

}
/**
 * 微信連接工廠
 * 
 * @author guosh
 *
 */
public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin> {
    
    /**
     * @param appId
     * @param appSecret
     */
    public WeixinConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter());
    }
    
    /**
     * 由于微信的openId是和accessToken一起返回的,所以在這里直接根據(jù)accessToken設(shè)置providerUserId即可搀菩,不用像QQ那樣通過QQAdapter來獲取
     */
    @Override
    protected String extractProviderUserId(AccessGrant accessGrant) {
        if(accessGrant instanceof WeixinAccessGrant) {
            return ((WeixinAccessGrant)accessGrant).getOpenId();
        }
        return null;
    }
    
    /* (non-Javadoc)
     * @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.oauth2.AccessGrant)
     */
    public Connection<Weixin> createConnection(AccessGrant accessGrant) {
        return new OAuth2Connection<Weixin>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),
                accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));
    }

    /* (non-Javadoc)
     * @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.connect.ConnectionData)
     */
    public Connection<Weixin> createConnection(ConnectionData data) {
        return new OAuth2Connection<Weixin>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
    }
    
    private ApiAdapter<Weixin> getApiAdapter(String providerUserId) {
        return new WeixinAdapter(providerUserId);
    }
    
    private OAuth2ServiceProvider<Weixin> getOAuth2ServiceProvider() {
        return (OAuth2ServiceProvider<Weixin>) getServiceProvider();
    }

    
}

/**
 * 
 * 完成微信的OAuth2認(rèn)證流程的模板類呕臂。國(guó)內(nèi)廠商實(shí)現(xiàn)的OAuth2每個(gè)都不同, spring默認(rèn)提供的OAuth2Template適應(yīng)不了,只能針對(duì)每個(gè)廠商自己微調(diào)肪跋。
 * 
 * @author guosh
 *
 */
public class WeixinOAuth2Template extends OAuth2Template {
    
    private String clientId;
    
    private String clientSecret;

    private String accessTokenUrl;
    
    private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";
    
    private Logger logger = LoggerFactory.getLogger(getClass());

    public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        setUseParametersForClientAuthentication(true);
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.accessTokenUrl = accessTokenUrl;
    }
    
    /* (non-Javadoc)
     * @see org.springframework.social.oauth2.OAuth2Template#exchangeForAccess(java.lang.String, java.lang.String, org.springframework.util.MultiValueMap)
     */
    @Override
    public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri,
                                         MultiValueMap<String, String> parameters) {
        
        StringBuilder accessTokenRequestUrl = new StringBuilder(accessTokenUrl);
        
        accessTokenRequestUrl.append("?appid="+clientId);
        accessTokenRequestUrl.append("&secret="+clientSecret);
        accessTokenRequestUrl.append("&code="+authorizationCode);
        accessTokenRequestUrl.append("&grant_type=authorization_code");
        accessTokenRequestUrl.append("&redirect_uri="+redirectUri);
        
        return getAccessToken(accessTokenRequestUrl);
    }
    
    public AccessGrant refreshAccess(String refreshToken, MultiValueMap<String, String> additionalParameters) {
        
        StringBuilder refreshTokenUrl = new StringBuilder(REFRESH_TOKEN_URL);
        
        refreshTokenUrl.append("?appid="+clientId);
        refreshTokenUrl.append("&grant_type=refresh_token");
        refreshTokenUrl.append("&refresh_token="+refreshToken);
        
        return getAccessToken(refreshTokenUrl);
    }

    @SuppressWarnings("unchecked")
    private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {
        
        logger.info("獲取access_token, 請(qǐng)求URL: "+accessTokenRequestUrl.toString());
        
        String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);
        
        logger.info("獲取access_token, 響應(yīng)內(nèi)容: "+response);
        
        Map<String, Object> result = null;
        try {
            result = new ObjectMapper().readValue(response, Map.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        //返回錯(cuò)誤碼時(shí)直接返回空
        if(StringUtils.isNotBlank(MapUtils.getString(result, "errcode"))){
            String errcode = MapUtils.getString(result, "errcode");
            String errmsg = MapUtils.getString(result, "errmsg");
            throw new RuntimeException("獲取access token失敗, errcode:"+errcode+", errmsg:"+errmsg);
        }
        
        WeixinAccessGrant accessToken = new WeixinAccessGrant(
                MapUtils.getString(result, "access_token"), 
                MapUtils.getString(result, "scope"), 
                MapUtils.getString(result, "refresh_token"), 
                MapUtils.getLong(result, "expires_in"));
        
        accessToken.setOpenId(MapUtils.getString(result, "openid"));
        
        return accessToken;
    }
    
    /**
     * 構(gòu)建獲取授權(quán)碼的請(qǐng)求歧蒋。也就是引導(dǎo)用戶跳轉(zhuǎn)到微信的地址。
     */
    public String buildAuthenticateUrl(OAuth2Parameters parameters) {
        String url = super.buildAuthenticateUrl(parameters);
        url = url + "&appid="+clientId+"&scope=snsapi_login";
        return url;
    }
    
    public String buildAuthorizeUrl(OAuth2Parameters parameters) {
        return buildAuthenticateUrl(parameters);
    }
    
    /**
     * 微信返回的contentType是html/text,添加相應(yīng)的HttpMessageConverter來處理谜洽。
     */
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }

}

/**
 * 
 * 微信的OAuth2流程處理器的提供器萝映,供spring social的connect體系調(diào)用
 * 
 * @author guosh
 *
 */
public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> {
    
    /**
     * 微信獲取授權(quán)碼的url
     */
    private static final String URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";
    /**
     * 微信獲取accessToken的url
     */
    private static final String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";

    /**
     * @param appId
     * @param appSecret
     */
    public WeixinServiceProvider(String appId, String appSecret) {
        super(new WeixinOAuth2Template(appId, appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN));
    }


    /* (non-Javadoc)
     * @see org.springframework.social.oauth2.AbstractOAuth2ServiceProvider#getApi(java.lang.String)
     */
    @Override
    public Weixin getApi(String accessToken) {
        return new WeixinImpl(accessToken);
    }

}

綁定解綁

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>綁定解綁</title>
</head>
<body>
<h2>標(biāo)準(zhǔn)綁定頁(yè)面</h2>
<!--    第一個(gè)值固定第二個(gè)是providerId   請(qǐng)求類型換成delete就是解除綁定-->
    <form action="/connect/weixin" method="post">
        <button type="submit">綁定微信</button>
    </form>
</body>
</html>

查詢當(dāng)前賬戶第三方綁定情況視圖

/**
 * 返回當(dāng)前賬戶哪些第三方登陸已經(jīng)綁定(綁定與解綁)
 * @Author: Guosh
 * @Date: 2019-06-03 14:09
 */
@Component("connect/status")
public class ConnectionStatusView extends AbstractView {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

        Map<String, List<Connection<?>>> connections = (Map<String, List<Connection<?>>>) model.get("connectionMap");

        Map<String, Boolean> result = new HashMap<>();
        for (String key : connections.keySet()) {
            result.put(key, CollectionUtils.isNotEmpty(connections.get(key)));
        }

        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

綁定與解綁

/**
 * 綁定成功返回視圖
 * @Author: Guosh
 * @Date: 2019-06-03 15:49
 */
public class ConnetView extends AbstractView {


    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.setContentType("text/html;charset=UTF-8");
        if (model.get("connections") == null) {
            response.getWriter().write("<h3>解綁成功</h3>");
        } else {
            response.getWriter().write("<h3>綁定成功</h3>");
        }
    }
}
@Configuration
@ConditionalOnProperty(prefix = "guosh.security.social.weixin", name = {"appId","appSecret"})
public class WeixinAutoConfiguration extends SocialAutoConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter
     * #createConnectionFactory()
     */
    @Override
    protected ConnectionFactory<?> createConnectionFactory() {
        WinxinProperties weixinConfig = securityProperties.getSocial().getWeixin();
        return new WeixinConnectionFactory(weixinConfig.getProviderId(), weixinConfig.getAppId(),
                weixinConfig.getAppSecret());
    }

    /**
     * 不帶ed是解綁的視圖帶ed是綁定帶視圖
     * @return
     */
    @Bean({"connect/weixinConnect","connect/weixinConnected"})
    @ConditionalOnMissingBean(name = "weixinConnectedView")
    public View weixinConnectedView() {
        return new ConnetView();
    }

}

Session管理

Session超時(shí)處理

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    //自定義配置文件
    @Autowired
    private SecurityProperties securityProperties;

    //登陸相關(guān)配置
    @Autowired
    private FormAuthenticationConfig formAuthenticationConfig;

    //校驗(yàn)驗(yàn)證碼
    @Autowired
    private ValidateCodeSecurityConfig validateCodeSecurityConfig;

    //手機(jī)號(hào)驗(yàn)證碼登陸方式
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    //登陸實(shí)現(xiàn)
    @Autowired
    private UserDetailsService userDetailsService;

    //第三方登陸
    @Autowired
    private SpringSocialConfigurer sociaSecurityConfig;

    //數(shù)據(jù)源
    @Autowired
    private DataSource dataSource;

    //處理session失效
    @Autowired
    private InvalidSessionStrategy invalidSessionStrategy;

    //處理最大登陸數(shù)
    @Autowired
    private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;


    //記住密碼
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        //啟動(dòng)自動(dòng)創(chuàng)建persistent_logins表
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        formAuthenticationConfig.configure(http);
        http
                .csrf().disable()//關(guān)閉跨站防護(hù)
                .apply(validateCodeSecurityConfig) //校驗(yàn)驗(yàn)證碼
                    .and()
                .apply(smsCodeAuthenticationSecurityConfig)//手機(jī)驗(yàn)證碼登陸
                    .and()
                .apply(sociaSecurityConfig) //第三方登陸
                    .and()
                .authorizeRequests()//下面授權(quán)配置
                    .antMatchers(
                            SecurityConstants.DEFAULT_UNAUTHENTICATION_URL, //處理登陸請(qǐng)求
                            SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_MOBILE, //手機(jī)登陸
                            securityProperties.getBrowser().getLoginPage(), //登陸頁(yè)面
                            securityProperties.getBrowser().getSignUpUrl(), //注冊(cè)頁(yè)面
                            SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*", //驗(yàn)證碼
                            "/user/regist")//第三方注冊(cè)跟綁定
                            .permitAll()//login請(qǐng)求除外不需要認(rèn)證
                    .anyRequest()
                    .authenticated()//所有請(qǐng)求都需要身份認(rèn)證
                    .and()
                .rememberMe() //記住密碼
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) //失效時(shí)間
                    .userDetailsService(userDetailsService)
                    .and()
                .sessionManagement()
                    .invalidSessionStrategy(invalidSessionStrategy) //session失效后的處理
                    .maximumSessions(securityProperties.getBrowser().getSession().getMaximumSessions()) //用戶最大登陸數(shù)
                    .maxSessionsPreventsLogin(securityProperties.getBrowser().getSession().isMaxSessionsPreventsLogin())//是否阻止登陸
                    .expiredUrl(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//用戶只能登陸一次
                    .expiredSessionStrategy(sessionInformationExpiredStrategy);//用戶被擠掉后的處理
    }
}
/**
 * @author guosh
 *
 */
public class AbstractSessionStrategy {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    /**
     * 跳轉(zhuǎn)的url
     */
    private String destinationUrl;
    /**
     * 重定向策略
     */
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    /**
     * 跳轉(zhuǎn)前是否創(chuàng)建新的session
     */
    private boolean createNewSession = true;
    
    private ObjectMapper objectMapper = new ObjectMapper();

    /**
     * @param invalidSessionUrl
     */
    public AbstractSessionStrategy(String invalidSessionUrl) {
        Assert.isTrue(UrlUtils.isValidRedirectUrl(invalidSessionUrl), "url must start with '/' or with 'http(s)'");
        this.destinationUrl = invalidSessionUrl;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.springframework.security.web.session.InvalidSessionStrategy#
     * onInvalidSessionDetected(javax.servlet.http.HttpServletRequest,
     * javax.servlet.http.HttpServletResponse)
     */
    protected void onSessionInvalid(HttpServletRequest request, HttpServletResponse response) throws IOException {

        if (createNewSession) {
            request.getSession();
        }

        String sourceUrl = request.getRequestURI();
        String targetUrl;

        //if (StringUtils.endsWithIgnoreCase(sourceUrl, ".html")) {
            targetUrl = destinationUrl;
            logger.info("session失效,跳轉(zhuǎn)到"+targetUrl);
            redirectStrategy.sendRedirect(request, response, targetUrl);
//      }else{
//          Object result = buildResponseContent(request);
//          response.setStatus(HttpStatus.UNAUTHORIZED.value());
//          response.setContentType("application/json;charset=UTF-8");
//          response.getWriter().write(objectMapper.writeValueAsString(result));
//      }
        
    }

    /**
     * @param request
     * @return
     */
    protected Object buildResponseContent(HttpServletRequest request) {
        String message = "session已失效";
        if(isConcurrency()){
            message = message + ",有可能是并發(fā)登錄導(dǎo)致的";
        }
        return new SimpleResponse(message);
    }

    /**
     * session失效是否是并發(fā)導(dǎo)致的
     * @return
     */
    protected boolean isConcurrency() {
        return false;
    }

    /**
     * Determines whether a new session should be created before redirecting (to
     * avoid possible looping issues where the same session ID is sent with the
     * redirected request). Alternatively, ensure that the configured URL does
     * not pass through the {@code SessionManagementFilter}.
     *
     * @param createNewSession
     *            defaults to {@code true}.
     */
    public void setCreateNewSession(boolean createNewSession) {
        this.createNewSession = createNewSession;
    }
    
}
public class MyInvalidSessionStrategy extends AbstractSessionStrategy implements InvalidSessionStrategy {

    /**
     * @param invalidSessionUrl
     */
    public MyInvalidSessionStrategy(String invalidSessionUrl) {
        super(invalidSessionUrl);
    }

    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        onSessionInvalid(request, response);
    }
}
public class MyExpiredSessionStrategy extends AbstractSessionStrategy implements SessionInformationExpiredStrategy {

    public MyExpiredSessionStrategy(String invalidSessionUrl) {
        super(invalidSessionUrl);
    }

    /* (non-Javadoc)
     * @see org.springframework.security.web.session.SessionInformationExpiredStrategy#onExpiredSessionDetected(org.springframework.security.web.session.SessionInformationExpiredEvent)
     */
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        onSessionInvalid(event.getRequest(), event.getResponse());
    }
    
    /* (non-Javadoc)
     * @see com.imooc.security.browser.session.AbstractSessionStrategy#isConcurrency()
     */
    @Override
    protected boolean isConcurrency() {
        return true;
    }

}

session集群處理

配置文件更改

  session:
    store-type: redis

退出登陸

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    private String signOutSuccessUrl;

    private Logger logger= LoggerFactory.getLogger(getClass());

    private ObjectMapper objectMapper = new ObjectMapper();

    private RedirectStrategy redirectStrategy=new DefaultRedirectStrategy();

    public MyLogoutSuccessHandler(String signOutSuccessUrl) {
        this.signOutSuccessUrl = signOutSuccessUrl;
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        logger.info("退出成功");
        if (StringUtils.isBlank(signOutSuccessUrl)) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("退出成功")));
        } else {
            redirectStrategy.sendRedirect(request,response,signOutSuccessUrl);
        }

    }
}

@Configuration
public class BrowserSecurityBeanConfig {

    @Autowired
    private SecurityProperties securityProperties;
    
    @Bean
    @ConditionalOnMissingBean(InvalidSessionStrategy.class)
    public InvalidSessionStrategy invalidSessionStrategy(){
        return new MyInvalidSessionStrategy(securityProperties.getBrowser().getSession().getSessionInvalidUrl());
    }
    
    @Bean
    @ConditionalOnMissingBean(SessionInformationExpiredStrategy.class)
    public SessionInformationExpiredStrategy sessionInformationExpiredStrategy(){
        return new MyExpiredSessionStrategy(securityProperties.getBrowser().getSession().getSessionInvalidUrl());
    }

    //退出賬號(hào)處理器
    @Bean
    @ConditionalOnMissingBean(LogoutSuccessHandler.class)
    public LogoutSuccessHandler logoutSuccessHandler(){
        return new MyLogoutSuccessHandler(securityProperties.getBrowser().getSignOutUrl());
    }
    
}

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    //自定義配置文件
    @Autowired
    private SecurityProperties securityProperties;

    //登陸相關(guān)配置
    @Autowired
    private FormAuthenticationConfig formAuthenticationConfig;

    //校驗(yàn)驗(yàn)證碼
    @Autowired
    private ValidateCodeSecurityConfig validateCodeSecurityConfig;

    //手機(jī)號(hào)驗(yàn)證碼登陸方式
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    //登陸實(shí)現(xiàn)
    @Autowired
    private UserDetailsService userDetailsService;

    //第三方登陸
    @Autowired
    private SpringSocialConfigurer sociaSecurityConfig;

    //數(shù)據(jù)源
    @Autowired
    private DataSource dataSource;

    //處理session失效
    @Autowired
    private InvalidSessionStrategy invalidSessionStrategy;

    //處理最大登陸數(shù)
    @Autowired
    private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;

    //集群session處理
    @Autowired
    private SessionRegistry sessionRegistry;
    //退出處理請(qǐng)求
    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;

    //記住密碼
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        //啟動(dòng)自動(dòng)創(chuàng)建persistent_logins表
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    @Bean
    public SessionRegistry getSessionRegistry(){
        SessionRegistry sessionRegistry=new SessionRegistryImpl();
        return sessionRegistry;
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        formAuthenticationConfig.configure(http);
        http
                .csrf().disable()//關(guān)閉跨站防護(hù)
                .apply(validateCodeSecurityConfig) //校驗(yàn)驗(yàn)證碼
                    .and()
                .apply(smsCodeAuthenticationSecurityConfig)//手機(jī)驗(yàn)證碼登陸
                    .and()
                .apply(sociaSecurityConfig) //第三方登陸
                    .and()
                .authorizeRequests()//下面授權(quán)配置
                    .antMatchers(
                            SecurityConstants.DEFAULT_UNAUTHENTICATION_URL, //處理登陸請(qǐng)求
                            SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_MOBILE, //手機(jī)登陸
                            securityProperties.getBrowser().getLoginPage(), //登陸頁(yè)面
                            securityProperties.getBrowser().getSignUpUrl(), //注冊(cè)頁(yè)面
                            SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*", //驗(yàn)證碼
                            "/user/regist")//第三方注冊(cè)跟綁定
                            .permitAll()//login請(qǐng)求除外不需要認(rèn)證
                    .anyRequest()
                    .authenticated()//所有請(qǐng)求都需要身份認(rèn)證
                    .and()
                .rememberMe() //記住密碼
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) //失效時(shí)間
                    .userDetailsService(userDetailsService)
                    .and()
                .sessionManagement()
                    .invalidSessionStrategy(invalidSessionStrategy) //session失效后的處理
                    .maximumSessions(securityProperties.getBrowser().getSession().getMaximumSessions()) //用戶最大登陸數(shù)
                    .maxSessionsPreventsLogin(securityProperties.getBrowser().getSession().isMaxSessionsPreventsLogin())//是否阻止登陸
                    .expiredUrl(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//用戶只能登陸一次
                    .expiredSessionStrategy(sessionInformationExpiredStrategy)//用戶被擠掉后的處理
                    .sessionRegistry(sessionRegistry)
                    .and()
                    .and()
                .logout()
                    .logoutUrl("/sigOut")//默認(rèn)退出路徑是logOut可以自定義
                    .logoutSuccessHandler(logoutSuccessHandler)//處理退出到類
                   //.logoutSuccessUrl("/login")//退出后跳到到頁(yè)面
                    .deleteCookies("JSESSIONID");


    }
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末阐虚,一起剝皮案震驚了整個(gè)濱河市序臂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌实束,老刑警劉巖奥秆,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異磕洪,居然都是意外死亡吭练,警方通過查閱死者的電腦和手機(jī)诫龙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門析显,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人签赃,你說我怎么就攤上這事谷异。” “怎么了锦聊?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵歹嘹,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我孔庭,道長(zhǎng)尺上,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任圆到,我火速辦了婚禮怎抛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘芽淡。我一直安慰自己马绝,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布挣菲。 她就那樣靜靜地躺著富稻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪白胀。 梳的紋絲不亂的頭發(fā)上椭赋,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音或杠,去河邊找鬼哪怔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蔓涧。 我是一名探鬼主播件已,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼元暴!你這毒婦竟也來了篷扩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤茉盏,失蹤者是張志新(化名)和其女友劉穎鉴未,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸠姨,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铜秆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了讶迁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片连茧。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖巍糯,靈堂內(nèi)的尸體忽然破棺而出啸驯,到底是詐尸還是另有隱情,我是刑警寧澤祟峦,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布罚斗,位于F島的核電站,受9級(jí)特大地震影響宅楞,放射性物質(zhì)發(fā)生泄漏针姿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一厌衙、第九天 我趴在偏房一處隱蔽的房頂上張望距淫。 院中可真熱鬧,春花似錦迅箩、人聲如沸溉愁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)拐揭。三九已至,卻和暖如春奕塑,著一層夾襖步出監(jiān)牢的瞬間堂污,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工龄砰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盟猖,地道東北人讨衣。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像式镐,于是被迫代替她去往敵國(guó)和親反镇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容