shiro+spring相关配置

首先pom中添加所需jar包:

    <!-- shiro start -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.2.3</version>
    </dependency>
    <!-- shiro end -->

一、在web.xml配制shiroFilter

    <!-- 配置Shiro过滤器,先让Shiro过滤系统接收到的请求 -->  
    <!-- 这里filter-name必须对应applicationContext.xml中定义的<bean id="shiroFilter"/> -->  
    <!-- 使用[/*]匹配所有请求,保证所有的可控请求都经过Shiro的过滤 -->  
    <!-- 通常会将此filter-mapping放置到最前面(即其他filter-mapping前面),以保证它是过滤器链中第一个起作用的 -->  
     <filter>  
        <filter-name>shiroFilter</filter-name>  
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>  
        <init-param>  
        <!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理   -->
        <param-name>targetFilterLifecycle</param-name>  
        <param-value>true</param-value>  
        </init-param>  
    </filter>  
    <filter-mapping>  
            <filter-name>shiroFilter</filter-name>  
            <url-pattern>/*</url-pattern>  
    </filter-mapping>

二、java代码编写

1.User.java

package isa.blog.bin.model;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import org.hibernate.annotations.GenericGenerator;

@Entity
public class User implements Serializable {
    private static final long serialVersionUID = -5312120825533005238L;
    
    @Id
    @GeneratedValue(generator="system-uuid")
    @GenericGenerator(name="system-uuid",strategy="uuid")
    private String id;
    
    @Column(unique = true)
    private String userName;
    
    private String password;
    
    private Date createTime;
    
    private String email;
    
    @ManyToOne
    @JoinColumn
    private Role role;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }
    
}

2.Role.java

package isa.blog.bin.model;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import org.hibernate.annotations.GenericGenerator;

@Entity
public class Role implements Serializable {
    private static final long serialVersionUID = -3431097890965814550L;

    @Id
    @GeneratedValue(generator="system-uuid")
    @GenericGenerator(name="system-uuid",strategy="uuid")
    private String id;
    
    private String roleName;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }
    
}

3.Permission.java

package isa.blog.bin.model;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import org.hibernate.annotations.GenericGenerator;

@Entity
public class Permission implements Serializable {
    private static final long serialVersionUID = 7808624074545962407L;

    @Id
    @GeneratedValue(generator="system-uuid")
    @GenericGenerator(name="system-uuid",strategy="uuid")
    private String id;
    
    private String permissionName;
    
    @ManyToOne
    @JoinColumn
    private Role role;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPermissionName() {
        return permissionName;
    }

    public void setPermissionName(String permissionName) {
        this.permissionName = permissionName;
    }

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }
}

4.UserDao.java

package isa.blog.bin.dao;

import org.springframework.data.jpa.repository.JpaRepository;

import isa.blog.bin.model.User;

public interface UserDao extends JpaRepository<User, String> {
    User findByUserNameAndPassword(String userName, String password);
    User findOneByUserName(String userName);
}

5.PermissionDao.java

package isa.blog.bin.dao;

import java.util.Set;

import org.springframework.data.jpa.repository.JpaRepository;

import isa.blog.bin.model.Permission;
import isa.blog.bin.model.Role;

public interface PermissionDao extends JpaRepository<Permission, String> {
    Set<Permission> findByRole(Role role);
}

6.MyRealm.java

package isa.blog.bin.commons;

import java.util.HashSet;
import java.util.Set;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import isa.blog.bin.dao.PermissionDao;
import isa.blog.bin.dao.UserDao;
import isa.blog.bin.model.Permission;
import isa.blog.bin.model.User;

@Transactional
public class MyRealm extends AuthorizingRealm{
    @Autowired
    private UserDao userDao;
    @Autowired
    private PermissionDao permissionDao;
    
    /**
     * 为当前登录的用户授予角色和权限
     */
    @Override
    public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String userName = (String)principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        try{
            authorizationInfo.addRole(userDao.findOneByUserName(userName).getRole().getRoleName());
            Set<Permission> permissions = permissionDao.findByRole(userDao.findOneByUserName(userName).getRole());
            Set<String> strs = new HashSet<String>();
            for (Permission permission : permissions) {
                strs.add(permission.getPermissionName());
            }
            authorizationInfo.addStringPermissions(strs);
        }catch(Exception e){
            e.printStackTrace();
        }
        return authorizationInfo;
    }

    /**
     * 验证当前登录的用户
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String userName = (String)token.getPrincipal();
        User user = userDao.findOneByUserName(userName);
        if(user!=null){
            AuthenticationInfo authcInfo=new SimpleAuthenticationInfo(user.getUserName(), user.getPassword(),"xx");
            return authcInfo;
        }else{
            return null;
        }
    }

}

三、配置文件applicationContext-shiro.xml

注意web.xml引入形式为:

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:/applicationContext*.xml</param-value>
    </context-param>

applicationContext-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <!-- 缓存管理器 -->
    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
    
    <!-- 继承自AuthorizingRealm的自定义Realm,即指定Shiro验证用户登录的类为自定义的ShiroDbRealm.java -->  
    <bean id="myRealm" class="isa.blog.bin.commons.MyRealm"/>  
    <!-- 踢出用户 -->
    <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.MemorySessionDAO" />
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="sessionDAO" ref="sessionDAO" />
    </bean>  
    <!-- Shiro默认会使用Servlet容器的Session,可通过sessionMode属性来指定使用Shiro原生Session -->  
    <!-- 即<property name="sessionMode" value="native"/>,详细说明见官方文档 -->  
    <!-- 这里主要是设置自定义的单Realm应用,若有多个Realm,可使用'realms'属性代替 -->  
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">  
        <property name="realm" ref="myRealm"/>
        <property name="sessionManager" ref="sessionManager" />
        <!-- 使用下面配置的缓存管理器 --> 
        <property name="cacheManager" ref="cacheManager"/> 
    </bean>  
      
    <!-- Shiro主过滤器本身功能十分强大,其强大之处就在于它支持任何基于URL路径表达式的、自定义的过滤器的执行 -->  
    <!-- Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截,Shiro对基于Spring的Web应用提供了完美的支持 -->  
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  
        <!-- Shiro的核心安全接口,这个属性是必须的 -->  
        <property name="securityManager" ref="securityManager"/>  
        <!-- 要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->  
        <property name="loginUrl" value="/views/login.html"/> 
        <!-- 登录成功后要跳转的连接(本例中此属性用不到,因为登录成功后的处理逻辑在LoginController里硬编码为main.jsp了) -->  
        <!-- <property name="successUrl" value="/index.html"/> --> 
        <!-- 用户访问未对其授权的资源时,所显示的连接 -->  
        <!-- 若想更明显的测试此属性可以修改它的值,如unauthor.jsp,然后用[玄玉]登录后访问/admin/listUser.jsp就看见浏览器会显示unauthor.jsp -->  
        <property name="unauthorizedUrl" value="/unauthorized.html"/>
        <!-- Shiro连接约束配置,即过滤链的定义 -->  
        <!-- 此处可配合我的这篇文章来理解各个过滤连的作用http://blog.csdn.net/jadyer/article/details/12172839 -->  
        <!-- 下面value值的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的 -->  
        <!-- anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种 -->  
        <!-- authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter -->  
        <property name="filterChainDefinitions">  
            <value>  
                 /frame/user/login/** = anon
                 /frame/user/register/** = anon
                 /frame/kaptcha** = anon
                 
                 /css/**  = anon
                 /img/**  = anon
                 /js/**  = anon
                 /json/**  = anon
                 /Scripts/**  = anon 

                 /views/login.html  = anon
                 /views/register.html  = anon
                 
                 # /frame/essay = roles[member]
                 # /views/addBlog.html = roles[admin]
                 # /frame/deleteEssay/** = perms[删除博客]
                 # /frame/deleteEssay/** = roles[admin]
                 
                 # 必须放在最后
                 /** = authc
            </value>  
        </property>
    </bean>  
      
    <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->  
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>  
      
    <!-- 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 -->  
    <!-- 配置以下两个bean即可实现此功能 -->  
    <!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run -->  
    <!-- 由于本例中并未使用Shiro注解,故注释掉这两个bean(个人觉得将权限通过注解的方式硬编码在程序中,查看起来不是很方便,没必要使用) -->  
    <!--   
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>  
      <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">  
        <property name="securityManager" ref="securityManager"/>  
      </bean>  
    -->

</beans>

以下代码根据实际项目更改

UserController.java

package isa.blog.bin.controller;

import java.awt.image.BufferedImage;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.Producer;

import isa.blog.bin.commons.JsonUtil;
import isa.blog.bin.commons.Result;
import isa.blog.bin.dto.UserDto;
import isa.blog.bin.service.UserService;

@RestController
@RequestMapping(value="/frame")
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private Producer producer;
    
    private Map<String, String> map; 
    
    @RequestMapping(value="/user/register/{kaptchaCode}", method=RequestMethod.POST, produces=JsonUtil.JSON)
    public Result register(@RequestBody UserDto userDto, @PathVariable String kaptchaCode) {
        return userService.register(userDto, map, kaptchaCode);
    }
    
    @RequestMapping(value="/user/oneUser/{userId}", method=RequestMethod.GET, produces=JsonUtil.JSON)
    public Result getOneUser(@PathVariable String userId) {
        return userService.getOneUser(userId);
    }
    
    @RequestMapping(value="/user/login/{userName}/{password}/{kaptchaCode}", method=RequestMethod.GET, produces=JsonUtil.JSON)
    public Result login(@PathVariable String userName, @PathVariable String password, @PathVariable String kaptchaCode) throws UnsupportedEncodingException {
        return userService.login(URLDecoder.decode(userName, "UTF-8"), password, map, kaptchaCode);
    }
    
    @RequestMapping(value="/user/logout", method=RequestMethod.GET, produces=JsonUtil.JSON)
    public void logout() {
        userService.logout();
    }
    
    @RequestMapping(value="/user/userDetails_userName", method=RequestMethod.GET, produces=JsonUtil.JSON)
    public Result getOneUserName() {
        return userService.getOneUserName();
    }
    
    @RequestMapping(value="/user/userDetails", method=RequestMethod.GET, produces=JsonUtil.JSON)
    public Result getOneUserDetails() {
        String userId = userService.currentUserId();
        return userService.getOneUser(userId);
    }
    
    @RequestMapping(value="/user/updateUserDetails", method=RequestMethod.POST, produces=JsonUtil.JSON)
    public Result updateUserDetails(@RequestBody UserDto userDto) {
        String userId = userService.currentUserId();
        return userService.updateUserDetails(userId, userDto);
    }
    
    @RequestMapping("/kaptcha")
    public void initCaptcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpSession session = request.getSession();
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
        response.setContentType("image/jpeg");
        String capText = producer.createText();
        session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
        BufferedImage bi = producer.createImage(capText);
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(bi, "jpg", out);
        try {
            out.flush();
        } finally {
            String kaptchaCode = (String)session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
            map = new HashMap<String, String>();
            map.put("kaptchaCode", kaptchaCode);
            out.close();
        }
    }
    
    
}

UserService.java

package isa.blog.bin.service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import isa.blog.bin.commons.MD5Util;
import isa.blog.bin.commons.Result;
import isa.blog.bin.dao.UserDao;
import isa.blog.bin.dto.UserDto;
import isa.blog.bin.model.User;

@Service
@Transactional
public class UserService {
    ModelMapper modelMapper = new ModelMapper();
    @Autowired
    private UserDao userDao;
    @Autowired
    private SessionDAO sessionDAO;
    
    /**
     * 注册
     * @param userDto
     * @param map
     * @param kaptchaCode
     * @return
     */
    public Result register(UserDto userDto, Map<String, String> map, String kaptchaCode) {
        Result result = new Result();
        if (!map.get("kaptchaCode").equals(kaptchaCode)) {
            result.setSuccess(false);
            result.setMessage("验证码错误");
            return result;
        }
        User user = userDao.findOneByUserName(userDto.getUserName());
        if (user != null) {
            result.setSuccess(false);
            result.setMessage("用户名已存在");
            return result;
        }
        user = modelMapper.map(userDto, User.class);
        user.setPassword(MD5Util.MD5(userDto.getPassword()));
        user.setCreateTime(new Date());
        userDao.save(user);
        result.setId(user.getId());
        result.setSuccess(true);
        result.setMessage("注册成功");
        return result;
    }
    
    /**
     * 获取一个用户详情
     * @param userId
     * @return
     */
    public Result getOneUser(String userId) {
        Result result = new Result(); 
        User user = userDao.findOne(userId);
        UserDto userDto = modelMapper.map(user, UserDto.class);
        List<UserDto> datas = new ArrayList<UserDto>(); 
        datas.add(userDto);
        result.setSuccess(true);
        result.setDatas(datas);
        return result;
    }
    
    /**
     * 登录
     * @param userName
     * @param password
     * @param map
     * @param kaptchaCode
     * @return
     */
    public Result login(String userName, String password, Map<String, String> map, String kaptchaCode) {
        Result result = new Result();
        if (!map.get("kaptchaCode").equals(kaptchaCode)) {
            result.setSuccess(false);
            result.setMessage("验证码错误");
            return result;
        }
        
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(userName, MD5Util.MD5(password));
        //踢除用户
        this.kickOutUser(token);
        try{
            subject.login(token);
            Session session = subject.getSession();
            System.out.println("sessionId:"+session.getId());
            System.out.println("sessionHost:"+session.getHost());
            System.out.println("sessionTimeout:"+session.getTimeout());
            result.setMessage("登录成功");
            System.out.println(userName+"登录成功");
            List<UserDto> userDtos = new ArrayList<UserDto>();
            User user = this.userDao.findOneByUserName(userName);
            userDtos.add(modelMapper.map(user, UserDto.class));
            result.setDatas(userDtos);
            result.setSuccess(true);
            return result;
        }catch(Exception e){
            e.printStackTrace();
            result.setSuccess(false);
            result.setMessage("用户名或密码错误!");
            System.out.println("用户名或密码错误!");
            return result;
        }
        
    }
    
    /**
     * 获得当前用户id
     * @return
     */
    public String currentUserId() {
        Subject subject = SecurityUtils.getSubject();
        PrincipalCollection collection = subject.getPrincipals();
        if (null != collection && !collection.isEmpty()) {
            String userName = (String) collection.iterator().next();
            return userDao.findOneByUserName(userName).getId();
        }
        return null;
    }
    
    /**
     * 踢除用户
     * http://www.ithao123.cn/content-7174367.html
     */
    public void kickOutUser(UsernamePasswordToken token){
        String loginName = token.getUsername();
        Collection<Session> sessions = sessionDAO.getActiveSessions();
        for(Session session:sessions){
            if(loginName.equals(String.valueOf(session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY)))) {
                //设置session立即失效,即将其踢出系统
                session.setTimeout(0);
            }
        }
    }
    
    /**
     * 退出登录
     */
    public void logout() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            // session 会销毁,在SessionListener监听session销毁,清理权限缓存
            subject.logout();
        }
    }
    
    /**
     * 获取当前用户名
     * @return
     */
    public Result getOneUserName() {
        Subject subject = SecurityUtils.getSubject();
        PrincipalCollection collection = subject.getPrincipals();
        Result result = new Result();
        if (null != collection && !collection.isEmpty()) {
            String userName = (String) collection.iterator().next();
            result.setSuccess(true);
            List<String> datas = new ArrayList<String>();
            datas.add(userName);
            result.setDatas(datas);
            return result;
        }
        result.setSuccess(false);
        return result;
    }
    
    /**
     * 修改用户信息
     * @param userId
     * @param userDto
     * @return
     */
    public Result updateUserDetails(String userId, UserDto userDto) {
        Result result = new Result();
        User user = modelMapper.map(userDto, User.class);
        user.setId(userId);
        user.setPassword(userDao.findOne(userId).getPassword());
        user.setCreateTime(userDao.findOne(userId).getCreateTime());
        user.setRole(userDao.findOne(userId).getRole());
        if (!userDto.getUserName().equals(userDao.findOne(userId).getUserName())) {
            result.setMessage("修改成功,立即重新登录!");
        } else {
            result.setMessage("修改成功!");
        }
        userDao.save(user);
        result.setId(userId);
        result.setSuccess(true);
        return result;
    }
    
}

需要注意,前端需要编写全局ajax以便对用户友好提示权限相关信息,

统一处理页面登录超时和无权限情况(一般在每一个页面都会调用的js文件中编写以便覆盖完整)

    //统一处理页面登录超时和无权限情况
    jQuery(function($){  
        // 备份jquery的ajax方法    
        var _ajax = $.ajax;
        // 重写ajax方法
        $.ajax=function(opt){ 
            var _error = opt && opt.error || function(a, b){};  
            var _opt = $.extend(opt, {  
                error:function(data, textStatus){  
                    // 如果后台将请求重定向到了登录页,则data里面存放的就是登录页的源码,这里需要找到data是登录页的证据(标记)  
                    if(data.responseText.indexOf("无此权限") > 0) {  
                        alert("无此权限");
                        return;
                    }  else if (data.responseText.indexOf("请输入用户名") > 0) {
                        alert("登录超时,请重新登录!");
                        window.location.href = '/views/login.html';
                        return;
                    } else {
                        alert("error");
                        return;
                    }
                    _error(data, textStatus);
                }
            });
            _ajax(_opt);  
        };  
    });
data.responseText获取得到的是applicationContext-shiro.xml里的配置
登录超时:
<property name="loginUrl" value="/views/login.html"/>
无权限:
<property name="unauthorizedUrl" value="/unauthorized.html"/>
对应页面的html的代码
原文地址:https://www.cnblogs.com/007sx/p/5727610.html