zoukankan      html  css  js  c++  java
  • 加入security+jwt安全策略

    Pom中引入

            <!-- security -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-test</artifactId>
                <scope>test</scope>
            </dependency>
            <!-- jwt -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.1</version>
            </dependency>
    

    加入后访问页面会弹出一个登录页,用户名为user,密码为启动时显示的一串字符串

     
    image.png

    但是这种页面及默认用户显然不是我们想要的,我们需要用户数据持久化,也需要合理分配权限。如下我们开始对security做基本的设置。

    关于security+jwt主要有如下几个类设置

    1.新建JwtUserDetails实现UserDetails

    package com.tangruo.example.common.security;
    
    import com.fasterxml.jackson.annotation.JsonIgnore;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.Collection;
    
    /**
     * 安全用户模型
     * @author 
     */
    public class JwtUserDetails implements UserDetails {
    
        private static final long serialVersionUID = 1L;
    
        /**
         * 用户名:这是数据库的字段,
         * 是userName或者是account就写对应的字段
         */
        private String username;
    
        /**
         * 密码
         */
        private String password;
        private String salt;
        /**
         *  权限集合
         */
        private Collection<? extends GrantedAuthority> authorities;
    
        JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
            this.username = username;
            this.password = password;
            this.salt = salt;
            this.authorities = authorities;
        }
    
        /**无论我数据库里的字段是 `account`,或者username,或者userName,或者其他代表账户的字段,
         * 这里还是要写成 `getUsername()`,因为是继承的接口
         *
         * @return
         */
        @Override
        public String getUsername() {
            return username;
        }
    
        @JsonIgnore
        @Override
        public String getPassword() {
            return password;
        }
    
        public String getSalt() {
            return salt;
        }
    
        /**
         * 返回给用户的角色列表
         * @return
         */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }
    
        /**
         * 账户是否未过期
         * @return
         */
        @JsonIgnore
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
        /**
         *账户是否未锁定
         * @return
         */
        @JsonIgnore
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
        /**
         *密码是否未过期
         * @return
         */
        @JsonIgnore
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
        /**
         *账户是否激活
         * @return
         */
        @JsonIgnore
        @Override
        public boolean isEnabled() {
            return true;
        }
    
    }
    

    2.新建UserDetailsServiceImpl实现UserDetailsService

    package com.rexyn.common.security;
    
    
    import com.rexyn.system.entity.User;
    import com.rexyn.system.service.AuthorityService;
    import com.rexyn.system.service.UserService;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    
    
    /**
     * 用户登录认证信息查询
     *
     * @author 
     */
    

    3.权限不足返回,新建AuthenticationAccessDeniedHandler实现AccessDeniedHandler类

    package com.tangruo.example.common.security;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.tangruo.example.common.api.ResultJson;
    
    import lombok.extern.slf4j.Slf4j;
    
    import org.apache.commons.httpclient.HttpStatus;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    /**
     * @author
     */
    @Component
    @Slf4j
    public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response,
                           AccessDeniedException accessDeniedException) throws IOException, ServletException {
            // 登陆状态下,权限不足执行该方法
            // log.error("权限不足:" + accessDeniedException.getMessage());
            response.setStatus(HttpStatus.SC_OK);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter printWriter = response.getWriter();
            //printWriter.println(objectMapper.writeValueAsString(Result.fail(HttpStatus.SC_FORBIDDEN, accessDeniedException.getMessage())));
            printWriter.println(objectMapper.writeValueAsString(ResultJson.fail(HttpStatus.SC_FORBIDDEN+"", "权限不足,无法访问")));
            printWriter.flush();
            printWriter.close();
        }
    
    }
    

    4.登陆失效设置,新建JwtAuthenticationEntryPoint实现AuthenticationEntryPoint

    package com.tangruo.example.common.security;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.io.Serializable;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.httpclient.HttpStatus;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.tangruo.example.common.api.ResultJson;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Component
    @Slf4j
    public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        /**
         * 
         */
        private static final long serialVersionUID = -4957913354424675827L;
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                AuthenticationException authException) throws IOException, ServletException {
            // 验证为未登陆状态会进入此方法,认证错误
    //      log.error("认证失败,请求接口:{},请求IP:{},请求参数:{},失败原因:{}", request.getRequestURI(), WebUtil.getIP(request),
    //              JsonUtil.toJson(request.getParameterMap()), authException.getMessage());
            response.setStatus(HttpStatus.SC_OK);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter printWriter = response.getWriter();
            //printWriter.write(objectMapper.writeValueAsString(Result.fail(HttpStatus.SC_FORBIDDEN, authException.getMessage())));
            printWriter.write(objectMapper.writeValueAsString(ResultJson.fail(HttpStatus.SC_FORBIDDEN+"", "登录已失效,请重新登录")));
            printWriter.flush();
            printWriter.close();
        }
    
    
    }
    

    5.登陆验证规则重写,新建JwtAuthenticationProvider继承DaoAuthenticationProvider

    package com.tangruo.example.common.security;
    
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    import com.tangruo.example.common.util.PasswordEncoder;
    
    
    /**重写身份验证规则
     * @author tangruo
     *
     * 2021年1月4日
     */
    public class JwtAuthenticationProvider extends DaoAuthenticationProvider{
    
        public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
            setUserDetailsService(userDetailsService);
         }
    
         @Override
        protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            if (authentication.getCredentials() == null) {
                logger.debug("Authentication failed: no credentials provided");
                throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
    
            String presentedPassword = authentication.getCredentials().toString();
            String salt = ((JwtUserDetails) userDetails).getSalt();
            // 覆写密码验证逻辑
            if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
                logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }
    

    6.新建WebSecurityConfig继承WebSecurityConfigurerAdapter

    package com.tangruo.example.common.config;
    
    
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
    import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
    
    import com.tangruo.example.common.security.AuthenticationAccessDeniedHandler;
    import com.tangruo.example.common.security.JwtAuthenticationEntryPoint;
    import com.tangruo.example.common.security.JwtAuthenticationProvider;
    
    
    /**
     * 全局@author Sheldon
     */
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        /**
         * Spring会自动寻找实现接口的类注入,会找到我们的 UserDetailsServiceImpl  类
         */
        @Qualifier("userDetailsServiceImpl")
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Autowired
        private AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;
    
        @Autowired
        private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    
    
        /**
         * anyRequest          |   匹配所有请求路径
         * access              |   SpringEl表达式结果为true时可以访问
         * anonymous           |   匿名可以访问
         * denyAll             |   用户不能访问
         * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
         * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
         * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
         * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
         * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
         * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
         * permitAll           |   用户可以任意访问
         * rememberMe          |   允许通过remember-me登录的用户访问
         * authenticated       |   用户登录后可访问
         */
    
    
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 使用自定义身份验证组件
            auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 进行授权成功后处理。授权成功后,重定向回之前访问的页面(获取RequestCache中存储的地址)
            SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
            successHandler.setTargetUrlParameter("redirectTo");
            // 允许页面内嵌显示
            http.headers().frameOptions().disable();
            
            // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
            http.cors().and().csrf().disable()
                    // 认证失败处理类
                    .exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler)
                    .and()
                    .exceptionHandling()
                    .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                    .and()
                    .authorizeRequests()
                    // 跨域预检请求
                    //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                    // 允许对于网站静态资源的无授权访问
                    .antMatchers(HttpMethod.GET,
                            
                            "/favicon.ico",
                            
                            "/**/*.css",
                            "/**/*.js",
                            "/**/*.jpg",
                            "/**/*.png"
                            
                    ).permitAll()
                    // 静态资源文件夹和swagger
                    .antMatchers("/lib/**").permitAll()
                    .antMatchers("/images/**").permitAll()
                    .antMatchers("/temp/**").permitAll()
                    
                    .antMatchers("/doc.html").permitAll()
                    .antMatchers("/swagger-ui.html").permitAll()
                    .antMatchers("/swagger-resources").permitAll()
                    .antMatchers("/v2/api-docs").permitAll()
                    .antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
                    .antMatchers("/swagger/user/login").permitAll()
                    // web jars
                    .antMatchers("/webjars/**").permitAll()
                    // 查看SQL监控(druid)
                    .antMatchers("/druid/**").anonymous()
                    .antMatchers("/home/**").permitAll()
                    .antMatchers("/finance/**").permitAll()
                    .antMatchers("/equipment/**").permitAll()
                    .antMatchers("/equipment/findMainEquipments").permitAll()
                    // 验证码
                    .antMatchers("/oauth/captcha**").permitAll()
                    .antMatchers("/layuiadmin/**").permitAll()
                    //.antMatchers("").hasAnyAuthority("'',''")
                    // 注册和登陆
                    .antMatchers("/pages/system/login.jsp").permitAll()
                    .antMatchers("/commons/*.jsp").permitAll()
                    .antMatchers("/").permitAll()
                    .antMatchers("/user/*").permitAll()
                    .antMatchers("/index").permitAll()
                    
                    //所有页面放开权限访问
                    .antMatchers("/pages/**").permitAll()
                    //配置生产管理接口访问权限登陆验证
                    .antMatchers("/production/**").permitAll()
                    
                    //其他所有请求需要身份认证
                    .anyRequest().authenticated()
    
                    // 无权限的时候调用AuthenticationAccessDeniedHandler
                    .and().exceptionHandling()
                    .accessDeniedHandler(getAccessDeniedHandler())
    
            ;
            // 退出登录处理器
            http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
            // token验证过滤器
            http.addFilterBefore(new BasicAuthenticationFilter(authenticationManager()),
                    UsernamePasswordAuthenticationFilter.class);
             }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
        @Bean
        public AccessDeniedHandler getAccessDeniedHandler() {
            return new AuthenticationAccessDeniedHandler();
        }
    }
    

    7.新建管理登陆接口类

    包含自定义登录接口,登出接口,主页跳转功能

    package com.tangruo.example.system.controller;
    
    import com.alibaba.fastjson.JSONObject;
    import com.tangruo.example.common.api.ResultJson;
    import com.tangruo.example.common.security.CookieUtil;
    import com.tangruo.example.common.security.JwtAuthenticatioToken;
    import com.tangruo.example.common.security.JwtTokenUtils;
    import com.tangruo.example.common.security.SecurityUtils;
    import com.tangruo.example.common.util.DateUtils;
    import com.tangruo.example.common.util.PasswordUtils;
    import com.tangruo.example.common.util.StringUtil;
    import com.tangruo.example.system.entity.AuthInfo;
    import com.tangruo.example.system.entity.Resource;
    import com.tangruo.example.system.entity.User;
    import com.tangruo.example.system.service.ResourceService;
    import com.tangruo.example.system.service.UserService;
    
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.*;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    import java.io.IOException;
    import java.net.URLEncoder;
    import java.util.List;
    @Api(value = "用户登陆/退出管理", tags = "用户登陆/退出管理")
    @Controller
    public class UserLandingController {
    
        private Logger logger = LoggerFactory.getLogger(this.getClass());
        @Autowired
        private UserService sysUserService;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private ResourceService resourceService;
       
        @ApiOperation(value = "跳转主页,做动态菜单处理")
        @RequestMapping("/index")
        public String index( HttpServletRequest request,HttpServletResponse res) {
            Authentication authentication=SecurityContextHolder.getContext().getAuthentication();
            String username=authentication.getName();
            if (authentication.getName().equals("anonymousUser")||authentication.getPrincipal() instanceof String) {
                return "/system/login";
            }
            List<Resource> resources=resourceService.findByUsername(username);
            logger.info(JSONObject.toJSONString(resources));
            request.setAttribute("resource", resources);
            
            
            return "/index";
        }
        
        @ApiOperation(value = "主页识别")
        @RequestMapping("/")
        public void defaultIndex( HttpServletRequest request,HttpServletResponse res) throws IOException{
            Authentication authentication=SecurityContextHolder.getContext().getAuthentication();
            
            if (authentication.getName().equals("anonymousUser")||authentication.getPrincipal() instanceof String) {
                res.sendRedirect("/pages/system/login.jsp?");
                return;
            }
            
            res.sendRedirect("/index");
        }
    
        @ApiOperation(value = "用户登陆")
        @PostMapping("/user/login")
        public void sysUserLogin( HttpServletRequest request,HttpServletResponse res) throws IOException{
            
            logger.info("the login is running");
            String username=request.getParameter("username");
            String password=request.getParameter("password");
            if (StringUtil.isBlank(username)||StringUtil.isBlank(password)) {
                String msg="请输入用户名或密码";
                msg=URLEncoder.encode(msg,"UTF-8");
                res.sendRedirect("/pages/system/login.jsp?msg="+msg);
             return;
            }
            User sysUser = sysUserService.findUser(username,null);
            ResultJson<Object> resultJson = this.login(username , password,sysUser);
            if (resultJson != null) {
                String msg=resultJson.getMsg();
                msg=URLEncoder.encode(msg,"UTF-8");
                res.sendRedirect("/pages/system/login.jsp?msg="+msg);
                return;
            }
            
            // 系统登录认证
            AuthInfo authInfo = this.createAuthInfo(request, username , password);
            if (authInfo == null) {
                String msg="凭证错误";
                msg=URLEncoder.encode(msg,"UTF-8");
                 res.sendRedirect("/pages/system/login.jsp?msg="+msg);
                 return;
            }
            if (!StringUtil.isEmpty(sysUser.getLoginDate())) {
                authInfo.setLastUpdateTime(sysUser.getLoginDate());
            }
            sysUser.setLoginDate(DateUtils.getCurrDate());
            sysUserService.saveOrUpdate(sysUser);
            
            sysUser.setPassword(null);
            sysUser.setName(sysUser.getName()==null?sysUser.getUsername():sysUser.getName());
            HttpSession session = request.getSession();
            session.setAttribute("currentUser", sysUser);
            res.sendRedirect("/index");
        }
        // 校验账号和密码
        public  ResultJson<Object> login(String username, String password,User sysUser) {
            String salt;
            String objectPassword;
            Integer status;
    
            // 用户信息
            if (sysUser == null) {
                return ResultJson.fail("TX000001" , "账号不存在");
            }
            salt = sysUser.getSalt();
            objectPassword = sysUser.getPassword();
            status = sysUser.getEnable();
            // 账号不存在、密码错误
            if (!PasswordUtils.matches(salt, password, objectPassword)) {
                return ResultJson.fail("TX000002" , "密码不正确");
            }
            // 账号锁定
            if (status == 0) {
                return ResultJson.fail("TX000003" , "账号已被锁定,请联系管理员");
            }
            return null;
        }
        /**
         * 生成AuthInfo
         *
         */
        public AuthInfo createAuthInfo(HttpServletRequest request, String userName, String password) {
            try {
                JwtAuthenticatioToken token = SecurityUtils.login(request, userName, password, authenticationManager);
                logger.info("the token is: " + token);
                CookieUtil.writeCookie("token" , token.getToken());
                CookieUtil.writeCookie("uid" , userName);
                AuthInfo authInfo = new AuthInfo();
                authInfo.setTokenType(SecurityUtils.BEARER);
                authInfo.setUserName(userName);
                authInfo.setToken(token.getToken());
                authInfo.setExpiresIn(JwtTokenUtils.getExpire());
                return authInfo;
            } catch (Exception e) {
                logger.error("生成AuthInfo出错" , e);
                return null;
            }
        }
        @ApiOperation(value = "登出" , notes = "退出登录")
        @GetMapping(value = "/user/logout")
        public void logout(HttpServletRequest request,HttpServletResponse response) throws IOException {
            String token = StringUtil.isEmpty(request.getHeader("token")) ? CookieUtil.getCookie(request, "token") : request.getHeader("token");
             String msg = "退出登录成功";
             msg=URLEncoder.encode(msg,"UTF-8");
             if (token == null) {
                msg="退出登录失败";
                 response.sendRedirect("/pages/system/login.jsp?msg="+msg);
                 return;
             }
             CookieUtil.removeCookie("token");
             response.sendRedirect("/pages/system/login.jsp?msg="+msg);
        }
    
    }
    

    我用的登陆失效返回都是json数据,如果需要返回登录页,可以对4中进行设置。
    有可能在上面的引用中会出现缺少类的问题。我这变引入了很多工具类,项目我重新打了一次压缩包(example-manage-2.0.zip)。可以进项目查看。
    下载地址 网盘: https://pan.baidu.com/s/1c44hr0uCfRI_693JII6ylA 提取码: ghbk
    返回目录springboot项目创建过程+maven+mybatis-plus+swagger+security
    下一章:十.权限的使用




  • 相关阅读:
    phpcms后台进入地址(包含No permission resources错误)
    phpmyadmin上传大sql文件办法
    ubuntu彻底卸载mysql
    Hdoj 2602.Bone Collector 题解
    一篇看懂词向量
    Hdoj 1905.Pseudoprime numbers 题解
    The Python Challenge 谜题全解(持续更新)
    Hdoj 2289.Cup 题解
    Hdoj 2899.Strange fuction 题解
    Hdoj 2199.Can you solve this equation? 题解
  • 原文地址:https://www.cnblogs.com/exmyth/p/14771185.html
Copyright © 2011-2022 走看看