shiro整合jwt

shiro整合jwt

這篇文章參考了這個(gè)網(wǎng)址:https://github.com/HowieYuan/Shiro-SpringBoot

一陪腌、添加依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.qianfeng</groupId>
    <artifactId>shiro-jwt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shiro-jwt</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

這是一個(gè)springboot項(xiàng)目娜扇,除了常規(guī)的springboot的依賴和數(shù)據(jù)庫的依賴选调,最重要的是shiro和jwt的依賴窃肠,分別是shiro-spring與java-jwt。

二士骤、項(xiàng)目結(jié)構(gòu)

[圖片上傳失敗...(image-6f4263-1590589772559)]

如上圖所示忆蚀,config包是配置包,ShiroConfig是Shiro的配置類嘁扼,用來對(duì)shiro進(jìn)行一些自定義配置信粮。filter是過濾器,因?yàn)槲覀兪褂昧薺wt所以需要自定義過濾器趁啸。然后是model强缘,由于這是一個(gè)前后端分離的項(xiàng)目,在向前端返回?cái)?shù)據(jù)的時(shí)候返回的是json格式的數(shù)據(jù)不傅,同時(shí)還要封裝一些狀態(tài)信息等旅掂,所以自定義這個(gè)類用來完成對(duì)狀態(tài)、對(duì)象的封裝访娶。shiro包下面的兩個(gè)類一個(gè)JWTToken是自定義的Token商虐,MyRealm是自定義的Realm。util包下的JWTUtil用來生成并校驗(yàn)token。

三称龙、JWTUtil

package com.qianfeng.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.io.UnsupportedEncodingException;
import java.util.Date;

/**
 * @author huwen
 */
public class JWTUtil {
    /**
     * 設(shè)置過期時(shí)間24小時(shí)
     */
    private static final long EXPIRE_TIME = 1000*60*60*24;
    /**
     * 設(shè)置密鑰
     */
    private static final String SECRET = "shiro+jwt";

    /**
     * 根據(jù)用戶名創(chuàng)建一個(gè)token
     * @param username 用戶名
     * @return 返回的token字符串
     */
    public static String createToken(String username){
        try {
            //將當(dāng)前時(shí)間的毫秒數(shù)和設(shè)置的過期時(shí)間相加生成一個(gè)新的時(shí)間
            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
            //由密鑰創(chuàng)建一個(gè)指定的算法
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            return JWT.create()
                    //附帶username信息
                    .withClaim("username",username)
                    //附帶過期時(shí)間
                    .withExpiresAt(date)
                    //使用指定的算法進(jìn)行標(biāo)記
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 驗(yàn)證token是否正確
     * @param token 前端傳過來的token
     * @param username 用戶名
     * @return 返回boolean
     */
    public static boolean verify(String token,String username){
        try {
            //獲取算法
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            //生成JWTVerifier
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username",username)
                    .build();
            //驗(yàn)證token
            verifier.verify(token);
            return true;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 從token中獲得username留拾,無需secret
     * @param token token
     * @return username
     */
    public static String getUsername(String token){
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}

這個(gè)類用來完成一些對(duì)token的操作:創(chuàng)建token、驗(yàn)證token鲫尊、從token中獲得username痴柔。創(chuàng)建token的時(shí)候需要指定token的過期時(shí)間,以及secret疫向,同樣咳蔚,驗(yàn)證的時(shí)候也需要secret。最后搔驼,從token中獲取username并不需要secret谈火。

四、JWTFilter

package com.qianfeng.filter;

import com.qianfeng.shiro.JWTToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;


public class JWTFilter extends BasicHttpAuthenticationFilter {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //判斷請(qǐng)求頭是否帶上“Token”
        if(isLoginAttempt(request, response)){
            //如果存在舌涨,則執(zhí)行executeLogin方法登入糯耍,檢查token是否正確
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                responseError(response,e.getMessage());
            }
        }
        //如果沒有token,則可能是執(zhí)行登錄操作或者是游客狀態(tài)訪問囊嘉,無需檢查token温技,直接返回true
        return true;
    }

    /**
     * 將非法請(qǐng)求跳轉(zhuǎn)到 /unauthorized/**
     * @param response
     * @param message
     */
    private void responseError(ServletResponse response, String message) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        try {
            message = URLEncoder.encode(message,"UTF-8");
            httpServletResponse.sendRedirect("/unauthorized/"+message);
        } catch (IOException e) {
            logger.error(e.getMessage());
        }
    }

    /**
     * 判斷用戶是否想要登入
     * 檢測(cè)header里面是否包含Token字段
     * @param request request
     * @param response response
     * @return boolean
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader("Token");
        return token != null;
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Token");
        JWTToken jwtToken = new JWTToken(token);
        //提交給realm進(jìn)行登入,如果錯(cuò)誤就會(huì)拋出異常并被捕獲
        getSubject(request, response).login(jwtToken);
        return true;
    }

    /**
     * 對(duì)跨域訪問提供支持
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域時(shí)會(huì)首先發(fā)送一個(gè)option請(qǐng)求扭粱,這里我們給option請(qǐng)求直接返回正常狀態(tài)
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

這個(gè)類繼承了BasicHttpAuthenticationFilter并重寫了里面的部分方法舵鳞。最重要的是executeLogin這個(gè)方法,這個(gè)方法從請(qǐng)求頭中獲取token這個(gè)字段然后構(gòu)造出一個(gè)JWTToken對(duì)象進(jìn)行登錄琢蛤,實(shí)際上就是提交給MyRealm蜓堕,和我們之前的UsernamePasswordToken登錄的方式一樣。

五博其、JWTToken

package com.qianfeng.shiro;

import org.apache.shiro.authc.AuthenticationToken;

public class JWTToken implements AuthenticationToken {
    private String token;
    public JWTToken(String token){
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

這個(gè)類實(shí)現(xiàn)了AuthenticationToken認(rèn)證Token這個(gè)接口套才,UsernamePasswordToken也是實(shí)現(xiàn)了這個(gè)接口,都可以用作認(rèn)證贺奠。

六霜旧、MyRealm

package com.qianfeng.shiro;

import com.qianfeng.pojo.Employee;
import com.qianfeng.pojo.Permission;
import com.qianfeng.pojo.Roles;
import com.qianfeng.service.EmployeeService;
import com.qianfeng.util.JWTUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Component("myRealm")
public class MyRealm extends AuthorizingRealm {
    @Resource
    private EmployeeService employeeService;

    /**
     * 必須重寫此方法,否則會(huì)報(bào)錯(cuò)
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token){
        return token instanceof JWTToken;
    }
    /**
     * 授權(quán)方法
     * @param principalCollection principal的集合儡率,可以理解為各種用戶身份的集合,比如用戶名以清、郵箱儿普、手機(jī)號(hào)等
     * @return 返回的是授權(quán)信息,包括角色與權(quán)限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = JWTUtil.getUsername(principalCollection.toString());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        List<Roles> roles = employeeService.getAllRolesByEmpName(username);
        Set<String> roleSet = new HashSet<>();
        Set<String> permissionSet = new HashSet<>();
        for (Roles role : roles) {
            roleSet.add(role.getRoleName());
        }
        List<Permission> permissions = employeeService.getAllPermissionsByEmpName(username);
        for (Permission permission : permissions) {
            permissionSet.add(permission.getPermName());
        }
        info.setRoles(roleSet);
        info.setStringPermissions(permissionSet);
        return info;
    }

    /**
     * 這個(gè)方法用于認(rèn)證
     * @param authenticationToken 用戶名與密碼
     * @return 認(rèn)證信息
     * @throws AuthenticationException 可能引發(fā)的異常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //獲得token
        String token = (String) authenticationToken.getCredentials();
        //從token中獲得username
        String username = JWTUtil.getUsername(token);
        //如果username為空或者驗(yàn)證不匹配
        if(username == null||!JWTUtil.verify(token,username)){
            throw new AuthenticationException("token認(rèn)證失敗!");
        }
        String password = employeeService.getPassword(username);
        //如果沒有查詢到用戶名對(duì)應(yīng)的密碼
        if(password==null){
            throw new AuthenticationException("該用戶不存在");
        }
        return new SimpleAuthenticationInfo(token,token,"MyRealm");
    }
}

MyRealm繼承了AuthorizingRealm掷倔,授權(quán)Realm眉孩,重寫其中的認(rèn)證和授權(quán)方法。

七、ShiroConfig

package com.qianfeng.config;

import com.qianfeng.filter.JWTFilter;
import com.qianfeng.shiro.MyRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //創(chuàng)建自定義過濾器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        //將JWTFilter命名為jwt
        filterMap.put("jwt",new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        //設(shè)置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //設(shè)置無權(quán)限時(shí)跳轉(zhuǎn)的url
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized/無權(quán)限");
        shiroFilterFactoryBean.setLoginUrl("/login");
        Map<String,String> filterRuleMap = new HashMap<>(2);
        //所有請(qǐng)求通過我們自己的過濾器
        filterRuleMap.put("/**","jwt");
        //匿名用戶可以訪問的url
        filterRuleMap.put("/unauthorized/**","anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilterFactoryBean;
    }
    @Bean
    public SecurityManager securityManager(MyRealm myRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm);
        //關(guān)閉shiro自帶的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 強(qiáng)制使用cglib浪汪,防止重復(fù)代理和可能引起代理出錯(cuò)的問題
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
}

這個(gè)類是Shiro的配置類巴柿,設(shè)置好我們自定義的 filter,并使所有請(qǐng)求通過我們的過濾器死遭,除了我們用于處理未認(rèn)證請(qǐng)求的 /unauthorized/**

八广恢、權(quán)限控制注解

主要通過shiro的@RequiresRoles和@RequiresPermissions注解進(jìn)行權(quán)限控制,這兩個(gè)注解放在controller的返回方法上呀潭,如果不具有相應(yīng)的角色或權(quán)限就會(huì)拋出異常

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钉迷,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子钠署,更是在濱河造成了極大的恐慌糠聪,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谐鼎,死亡現(xiàn)場(chǎng)離奇詭異舰蟆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)狸棍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門夭苗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人隔缀,你說我怎么就攤上這事题造。” “怎么了猾瘸?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵界赔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我牵触,道長(zhǎng)淮悼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任揽思,我火速辦了婚禮袜腥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钉汗。我一直安慰自己羹令,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布损痰。 她就那樣靜靜地躺著福侈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卢未。 梳的紋絲不亂的頭發(fā)上肪凛,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天堰汉,我揣著相機(jī)與錄音,去河邊找鬼伟墙。 笑死翘鸭,一個(gè)胖子當(dāng)著我的面吹牛戳葵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播譬淳,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼守伸,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼浦妄!你這毒婦竟也來了剂娄?” 一聲冷哼從身側(cè)響起阅懦,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤耳胎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后废登,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體郁惜,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兆蕉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年半醉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了劝术。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片养晋。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绳泉,死狀恐怖零酪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情炒事,我是刑警寧澤蟀架,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布片拍,位于F島的核電站捌省,受9級(jí)特大地震影響碉钠,放射性物質(zhì)發(fā)生泄漏放钦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望颓屑。 院中可真熱鬧揪惦,春花似錦器腋、人聲如沸钩杰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胸嘁。三九已至性宏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蝌借,已是汗流浹背菩佑。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工稍坯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留搓劫,地道東北人枪向。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓秘蛔,卻偏偏與公主長(zhǎng)得像深员,于是被迫代替她去往敵國(guó)和親倦畅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348