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
    下一章:十.权限的使用




  • 相关阅读:
    如遇临时表无法删除
    ORA-00054:resource busy and acquire with nowait specified解决方法
    查看用户建立详细原语句
    dbtool部署
    启动uiautomatorview 提示无法初始化主类
    运行虚拟机报错:CPU acceleration status: HAXM is not installed on this machine
    Appium-Python-Windows 环境搭建
    Vagrant安装Docker
    XPath
    Selenium之元素定位
  • 原文地址:https://www.cnblogs.com/exmyth/p/14771185.html
Copyright © 2011-2022 走看看